Fix ADV transition and nested input lifecycles

This commit is contained in:
gamer147
2026-07-29 18:16:09 -04:00
parent 7cb0e879e5
commit 3c8aed1c16
13 changed files with 251 additions and 78 deletions

View File

@@ -2013,15 +2013,13 @@ 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`.
**Skip pacing regression corrected (2026-07-29).** A later port change incorrectly treated
`adv_interpreter_tick`'s one-op dispatch as one rendered frame per opcode only while Skip was active.
That contradicted the synchronized native trace above: the outer service calls the dispatcher in fast bursts,
and Ctrl measured about 7,738 operand fetches/sec rather than one per display frame. The artificial
`GodotAdvHost.FrameYield` wait made the setup/cleanup bytecode between skipped fades take seconds even though
the fades themselves correctly jumped to their endpoints. `FrameYield` is nonblocking again; only explicit
presentation, sleep, and input services own wall-clock time.
**ADV entry/exit lifecycle.** The script comments `savemesskip` and `loadmesskip` describe a temporary
suspension boundary, not a second saved preference. `op_0x19b_suspend_adv_skip_service@0x416560` clears
@@ -3454,6 +3452,13 @@ before `gfx_render_frame`. This is the generalized native CG-fade click path. It
color/alpha and matrix channels serviced by `0x21c`, as well as queued type-0 surface commands; it is separate
from the legacy run-state-bit-8 fade service.
The ordering is a lifecycle contract, not incidental: native clears run-state `0x400` before calling
`gfx_request_force_complete_and_reset_anim_clock`. Service-flags bit 1 can make that worker a no-op, and
detached or movie-backed presentation can remain active, but none of those states re-park the interpreter.
The port must therefore track “the `0x400` wait was bypassed” separately from “all timed presentation is
inactive.” Deriving the wait solely from the latter produces an invisible-duration stall: ordinary fade
channels reach their endpoint, while an excluded movie/protected channel keeps the VM blocked.
Corpus evidence is unusually sharp: all 303 calls pass an immediate flag, with zero used 302 times and one
used once. SC0000's two CG-loader sites (`0x12723`, `0x13310`) write zero. `BTL.BIN@0x2b4d` writes one on an
animated battle object after its texture/movie or sprite-cell setup, matching the nonblocking background
@@ -3679,10 +3684,12 @@ uses callback slot `count` only for an empty mask. Godot translates layout-indep
native Win32 VK namespace and sends physical mouse/standard joy events through the service; it no longer
assigns Godot's `ui_*` actions directly to AGE indices. Direct-scene diagnostics replay the same 16 immediate
SYSTEM4 configuration calls through `InputBindingBootstrap`, while natural boot executes the real opcodes.
The live action-6 state now also feeds the ADV fast-forward host channel while the ADV lifecycle service is
enabled. That transient channel is kept separate from persistent op-`0x88` Skip, so releasing the held key
cannot clear the user's toggle; both channels share the existing text completion, wait advance, voice
deferral, and skip cadence. This needs no profile storage, boot seed, or game-specific conditional.
The live action-6 state now also feeds the ADV fast-forward host channel independently of the
op-`0x19b`/`0x19c` lifecycle gate. Ghidra shows `adv_interpreter_tick` polling the physical action mask before
dispatch on every tick and testing bit `0x40` without consulting `adv_skip_service_enabled`; `0x19b` clears
persistent/read-driven active Skip but cannot release a physically held key. The transient physical channel
remains separate from persistent op-`0x88` Skip, so releasing the key cannot clear the user's toggle. Both
channels share text completion, wait advance, and voice deferral.
### ADV right-click/X system-menu path (2026-07-21)

View File

@@ -734,7 +734,7 @@ Port status (2026-07-24): the VM refreshes this state from message:ReadTextSkip
- **depended on by:** 0x223
- **evidence:** Ghidra handler 0x417520 records the 1-dword instruction length and ORs ctx+0xa0ce4 with 0x400. engine_main_tick_with_exception_policy@0x411840 branch 0x4123d1 polls gfx_animation_service_poll; its active branch reads EffectSkipOnClick@0x570f60, rejects gfx_animation_service_flags bit 0, polls/consumes action-4 bit 0x10, clears run-state 0x400, calls gfx_request_force_complete_and_reset_anim_clock@0x4076c0, then gfx_render_frame. capture_presentation_trace.py: after 0x125a6 render, 0xcb8e/0xcb98 bind and 0xd5a/0xd63/0xd73/0xd8a mode+targets execute without render; repeated gfx_render_frame begins only at 0x21c. 2026-07-10, click lifecycle refined 2026-07-29.
SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit 0x400 parks the interpreter while gfx_render_frame repeatedly samples finite one-shot object channels and queued surface commands; op 0x224 follows after dirty state clears. When system:EffectSkipOnClick is enabled and animation-service flag bit 0 is clear, logical action 4 clears the service wait, requests the same forced completion as 0x243, renders the terminal retained frame, and consumes the trigger. That request completes ordinary finite color/scale/rotation/translation channels and type-0 surface commands together; op-0x242-detached channels, ambient cycles, and movie masks are excluded. Native trace proves AE001D bind, mode-1 0x203, and 0x202 targets complete in one 5 ms batch with no render, then first compose here.
SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit 0x400 parks the interpreter while gfx_render_frame repeatedly samples finite one-shot object channels and queued surface commands; op 0x224 follows after dirty state clears. When system:EffectSkipOnClick is enabled and animation-service flag bit 0 is clear, logical action 4 is consumed and run-state 0x400 is cleared before the engine requests the same forced completion as 0x243 and renders once. Wait release is independent of endpoint completion: service-flags bit 1 can suppress the force worker, while op-0x242-detached channels, ambient cycles, and movie-backed presentation can continue asynchronously after interpreter resume. With ordinary flags, the request completes finite color/scale/rotation/translation channels and type-0 surface commands together. Native trace proves AE001D bind, mode-1 0x203, and 0x202 targets complete in one 5 ms batch with no render, then first compose here.
## draw
@@ -1341,12 +1341,14 @@ Port status (2026-07-29): implemented as unsigned scene-context state. The sette
- **evidence:** Ghidra /v2: op_0x19a_get_message_skip@0x4271b0 writes ctx+0x550fc to operand 1. The shared ADV redraw routine uses it to select SO001's active Message-skip icon at x=728.
### 0x19b `suspend-adv-skip-service` (u00414E80, argc 0)
- **summary:** () - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle.
- **summary:** () - suspend persistent/read-driven ADV fast-forward while preserving the user's all-message Skip toggle; physically held logical action 6 is independent.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x19b_suspend_adv_skip_service@0x416560 clears run-state bit 0x08000000, ctx+0x13dc active message skip, ctx+0x55100 ADV skip-service enable, and the click-cancel phase, but deliberately leaves ctx+0x550fc persistent all-message Skip unchanged. Corpus comments call it `savemesskip`; ADV-end/branch and CALLBACK_LOAD paths use it before leaving ADV presentation.
- **evidence:** Ghidra /v2: op_0x19b_suspend_adv_skip_service@0x416560 clears run-state bit 0x08000000, ctx+0x13dc active message skip, ctx+0x55100 ADV skip-service enable, and the click-cancel phase, but deliberately leaves ctx+0x550fc persistent all-message Skip unchanged. It does not mutate the input manager; adv_interpreter_tick@0x410fb0 polls logical action 6 (mask bit 0x40) independently of ctx+0x55100. Corpus comments call the opcode `savemesskip`; ADV-end/branch and CALLBACK_LOAD paths use it before leaving ADV presentation.
This lifecycle operation affects persistent/read-driven active Skip only. Physical logical action 6 is process input polled independently by adv_interpreter_tick and remains active while held.
### 0x19c `resume-adv-skip-service` (u00414EC0, argc 0)
- **summary:** () - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.
- **summary:** () - enable the persistent/read ADV skip service and recompute active fast-forward from all-message Skip or the live read-skip channel; physical action 6 remains independent.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x19c_resume_adv_skip_service@0x4165a0 sets ctx+0x55100, clears the click-cancel phase, and normally activates run-state bit 0x08000000 when ctx+0x550fc persistent all-message Skip or ctx+0x6dbd4 adv_read_skip_state is nonzero. Otherwise it clears ctx+0x13dc and the run-state bit. A separate startup fast-forward guard at ctx+0x6f86c suppresses reactivation while active. Corpus comments call it `loadmesskip`; ADV setup, branch, and CALLBACK_LOAD paths pair it with op 0x19b.

View File

@@ -874,6 +874,42 @@ activating Ctrl/Skip during the wait uses the same endpoint path. Ambient loops,
and movie masks remain unaffected. Focused exclusions and endpoint regressions pass in the 523-test engine
suite, and the Godot build is warning-free.
**Retained transition wait ownership corrected (2026-07-29):** playthrough testing with Skip held exposed
that endpoint completion alone was insufficient. Skip could force an ordinary fade invisible, yet the host
continued waiting on the broad presentation predicate until a movie-backed/protected channel or the original
duration ended. Native clears run-state `0x400` before requesting endpoint completion, so interpreter resume
is independent of whether excluded presentation remains active. The host now records that explicit service
bypass, exits the transition wait immediately when the action is accepted, and reapplies an already-active
Skip state at every new retained-transition boundary. Focused coverage distinguishes service-flags bit 0
(reject the click) from bit 1 (accept resume but suppress endpoint forcing) and proves movie masks continue
asynchronously after wait bypass. A direct `SC0000@0xc01` runtime trace—the page before the ritual CG
sequence—shows its four click-skipped retained waits completing in 14, 14, 14, and 34 ms despite scripted
durations of 150/890/300 ms; the later 1.587-second interval is the next page's ordinary glyph reveal at
`SC0000@0xe04`, not an invisible transition wait.
**Held-Skip pacing corrected (2026-07-29):** comparing held logical action 6 against the click trace at
`SC0000@0xc01` exposed a separate delay outside the transition service. The Godot host was waiting for one
rendered-frame pulse after every VM opcode while Skip was active, stretching hundreds of burst-fast CG
setup/cleanup instructions into an invisible multi-second hold. Native live evidence instead measures roughly
7,738 operand fetches/sec under Ctrl, and Ghidra shows the physical action-6 mask polled independently of the
`0x19b`/`0x19c` persistent-Skip lifecycle. Per-op Skip pacing is removed, and a held Ctrl/C/Backspace channel
now survives `0x19b`; presentation, sleep, and input boundaries remain the only wall-clock owners. The
post-fix held-action trace reaches page 14 from `SC0000@0xc01` in 83 ms and takes the native
`0x243`+`0x20c` endpoint branch at every intervening transition. The engine suite passes 525/525.
**Nested ADV input ownership corrected (2026-07-29):** the first-dungeon boss event reaches ordinary
dialogue at `SC0600@0x2343` and parks at `wait-for-input@0x234b`, but FIELD's registered timed mouse
callback remains stored in its suspended parent script frame. The frontend incorrectly treated any stored
raw callback as current modal input ownership. It consequently hid the wait indicator and withheld normal
mouse/keyboard/joy advance signals from the nested SC0600 page, producing a softlock immediately before
combat.
Raw callback ownership is now frame-relative: it is active only while its owning script frame is the
currently executing frame. The dormant FIELD registration survives the nested call and becomes active
again when FIELD resumes, while SC0600 retains its ordinary ADV marker and input path. A synthetic
parent-callback → child-dialogue regression preserves both halves of that lifetime. The engine suite passes
526/526, the Godot build has zero warnings, and the Himegari-targeted threaded selftest passes.
**Cyclic reset implemented (2026-07-29):** `0x230(handle)` now gets or creates the retained object,
disables the four looping channels represented by the compositor, and clears the complete native
start/period block—including the preserved raw state for the currently unmodeled second cyclic matrix.

View File

@@ -362,6 +362,9 @@ use the last `input-wait` event in `timeline.jsonl` as the authoritative manual
and `colorProgress`, synchronized with the same frame/clock in `--timeline-log`. Parent directories are
created automatically.
- `--transition-click-ms <n>` — diagnostic-only input injector: after a foreground transition has been active for `n` virtual milliseconds, send one click through the real input lifecycle. The click completes/consumes the transition and does not advance a stable page. Use with `--timeline-log`, `--gfx-log`, and windowed `--shot-sequence`; omit for normal play.
- `--hold-message-skip` — diagnostic-only held-input injector: hold Ctrl through the VM's real keyboard
binding path, producing native logical action 6 continuously. Use it to compare persistent fast-forward
against `--transition-click-ms` at the same page without relying on synthetic host state.
- `--timeline-log <jsonl>` — diagnostic-only synchronized event stream for a real Godot run. Records every
executed script byte offset/opcode, script-frame entry/exit with depth/cause/outcome, resolved call-script
ids/names, unknown-fallback `stub` events, virtual time/frame, VM state changes (`running`, `sleep`, `input-wait`,

View File

@@ -111,7 +111,8 @@ public class ForegroundTransitionTests
gfx.QueueMovieMaskTransition(new MovieMaskTransitionRequest(
11, 45, 10, 1, -184, 0, 800, 600, 0, 0x325e, 0, 1000));
Assert.Equal(3, gfx.CompleteClickSkippableTimedPresentation(200));
Assert.True(gfx.TryCompleteClickSkippableTimedPresentation(200, out int completed));
Assert.Equal(3, completed);
Assert.Equal(1.0, gfx.SnapshotForegroundTransitions(200).Single().Progress);
Assert.False(gfx.TryGet(100)!.OneShotColorEnabled);
@@ -123,9 +124,8 @@ public class ForegroundTransitionTests
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void AnimationServiceFlagsSuppressClickCompletion(long flags)
public void AnimationServiceFlagBit0RejectsClickCompletion(long flags)
{
var gfx = new GfxState();
gfx.SetSurface(4, 0x25, -1);
@@ -133,7 +133,35 @@ public class ForegroundTransitionTests
gfx.SetAnimatedObjectColorResolved(100, 0, 1000, 0, 0xffffff);
gfx.SetAnimationServiceFlags(flags);
Assert.Equal(0, gfx.CompleteClickSkippableTimedPresentation(100));
Assert.False(gfx.TryCompleteClickSkippableTimedPresentation(100, out int completed));
Assert.Equal(0, completed);
Assert.True(gfx.TryGet(100)!.OneShotColorEnabled);
}
[Fact]
public void AnimationServiceFlagBit1AcceptsWaitBypassWithoutForcingEndpoint()
{
var gfx = new GfxState();
gfx.SetSurface(4, 0x25, -1);
gfx.BindDraw(100, 4, 0, 0, 800, 600, 0, 0);
gfx.SetAnimatedObjectColorResolved(100, 0, 1000, 0, 0xffffff);
gfx.SetAnimationServiceFlags(2);
Assert.True(gfx.TryCompleteClickSkippableTimedPresentation(100, out int completed));
Assert.Equal(0, completed);
Assert.True(gfx.TryGet(100)!.OneShotColorEnabled);
}
[Fact]
public void MovieMaskAcceptsWaitBypassAndContinuesAsynchronously()
{
var gfx = new GfxState();
gfx.QueueMovieMaskTransition(new MovieMaskTransitionRequest(
11, 45, 10, 1, -184, 0, 800, 600, 0, 0x325e, 0, 1000));
Assert.True(gfx.TryCompleteClickSkippableTimedPresentation(100, out int completed));
Assert.Equal(0, completed);
Assert.True(gfx.HasActiveTimedPresentation(100));
Assert.False(gfx.SnapshotMovieMaskTransitions().Single().Completed);
}
}

View File

@@ -85,7 +85,7 @@ public class InputBindingTests
Sys4ScriptProvider.Load(Table).RequireByName("SYSTEM4.BIN"), vm.InputBindings);
vm.UpdateKeyboardVirtualKeyState(virtualKey, true);
Assert.False(host.PhysicalMessageSkip); // ADV lifecycle is not active yet.
Assert.True(host.PhysicalMessageSkip);
vm.Run();
@@ -98,6 +98,29 @@ public class InputBindingTests
Assert.False(host.MessageSkip);
}
[Fact]
public void HeldLogicalActionSixSurvivesPersistentSkipLifecycleSuspension()
{
var script = ScriptAssembler.Assemble(Table, "HELD_SKIP_LIFECYCLE", new List<(int, Operand[])>
{
(0x19c, Array.Empty<Operand>()),
(0x19b, Array.Empty<Operand>()),
(0x1c7, new[] { G(0x140) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var host = new RecordingHost();
var vm = new VirtualMachine(script, Table, host);
InputBindingBootstrap.Apply(
Sys4ScriptProvider.Load(Table).RequireByName("SYSTEM4.BIN"), vm.InputBindings);
vm.UpdateKeyboardVirtualKeyState(0x11, true);
vm.Run();
Assert.True(host.PhysicalMessageSkip);
Assert.True(host.MessageSkip);
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x140));
}
[Fact]
public void ReleasingPhysicalFastForwardDoesNotClearPersistentSkip()
{

View File

@@ -6,6 +6,28 @@ using Xunit;
public class WaitForInputTests
{
private sealed class NestedWaitHost : RecordingHost
{
public VirtualMachine Vm = null!;
public bool SawDormantParentCallback;
public bool SawParentCallbackRestored;
public override void WaitForInput(int layoutSlot, System.Func<bool> serviceInputCallback)
{
Waits++;
SawDormantParentCallback =
Vm.RawInputCallbackScriptName == "PARENT"
&& !Vm.IsRawInputCallbackActive;
}
public override void Sleep(long duration)
{
SawParentCallbackRestored =
Vm.RawInputCallbackScriptName == "PARENT"
&& Vm.IsRawInputCallbackActive;
}
}
// wait-for-input (0x72) fires per page. Synthesize a two-page scene and assert it fires exactly
// twice — full handling, no dependency on a real scene's (stubbed) line count.
[Fact]
@@ -28,4 +50,35 @@ public class WaitForInputTests
Assert.Equal(new[] { "page one", "page two" }, vm.Emitted.Select(e => e.Text).ToArray());
Assert.Equal("exit", vm.HaltReason);
}
[Fact]
public void DormantParentRawInputCallbackDoesNotOwnNestedDialogueWait()
{
var t = OpcodeTableJson.Load(Paths.OpcodesJson);
var child = ScriptAssembler.Assemble(t, "CHILD", new List<(int, Operand[])>
{
(0x6e, new[] { new Operand(2, 0), new Operand(0, 0) }),
(0x72, new[] { new Operand(0, 0) }),
(0x2, System.Array.Empty<Operand>()),
}, new[] { "nested dialogue" });
var parent = ScriptAssembler.Assemble(t, "PARENT", new List<(int, Operand[])>
{
// FIELD-style timed mouse callback remains registered while call-script enters an ADV scene.
(0xcc, new[] { new Operand(0, 50), new Operand(0, 0xffff_ffff) }),
(0x3, new[] { new Operand(0, 5) }),
(0xc8, new[] { new Operand(0, 0) }),
(0x2, System.Array.Empty<Operand>()),
}, System.Array.Empty<string>());
var host = new NestedWaitHost();
var vm = new VirtualMachine(parent, t, host, provider: new MapProvider(new() { [5] = child }));
host.Vm = vm;
vm.Run();
Assert.True(host.SawDormantParentCallback);
Assert.True(host.SawParentCallbackRestored);
Assert.Equal(1, host.Waits);
Assert.False(vm.IsRawInputCallbackActive);
Assert.Equal("exit", vm.HaltReason);
}
}

View File

@@ -934,38 +934,41 @@ public sealed class GfxState
(o.RotationEnabled && o.RotationPeriodMs > 0)));
}
/// <summary>Force the finite retained presentation serviced by native run-state bit 0x400 to its
/// endpoint. This is the EffectSkipOnClick path: type-0 surface commands and ordinary finite object
/// channels complete together, while movie masks, ambient cycles, and op-0x242-detached channels remain
/// active. Op 0x24e bit 0 disables click skipping; bit 1 suppresses the shared force-complete request.</summary>
public int CompleteClickSkippableTimedPresentation(long nowMs)
/// <summary>Consume the native run-state-0x400 EffectSkipOnClick action and, when permitted, force its
/// finite retained presentation to the endpoint. Bit 0 of op 0x24e rejects the action entirely. Bit 1
/// still allows the service wait to end but suppresses the shared force-complete request. Movie masks,
/// ambient cycles, and op-0x242-detached channels continue asynchronously after the service resumes.</summary>
public bool TryCompleteClickSkippableTimedPresentation(long nowMs, out int completed)
{
lock (_lock)
{
if ((AnimationServiceFlags & 3) != 0) return 0;
completed = 0;
if ((AnimationServiceFlags & 1) != 0) return false;
int completed = 0;
foreach (var t in _surfaceTransitions.Values)
if ((AnimationServiceFlags & 2) == 0)
{
if (t.Forced || TransitionProgress(t, nowMs) >= 1.0) continue;
t.Forced = true;
completed++;
}
foreach (var t in _surfaceTransitions.Values)
{
if (t.Forced || TransitionProgress(t, nowMs) >= 1.0) continue;
t.Forced = true;
completed++;
}
completed += CountOneShotChannels(_rangeTransform);
foreach (var o in _objects.Values)
{
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
completed += CountOneShotChannels(o);
}
ForceCompleteOneShotChannels();
completed += CountOneShotChannels(_rangeTransform);
foreach (var o in _objects.Values)
{
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
completed += CountOneShotChannels(o);
}
ForceCompleteOneShotChannels();
if (AnimClockDurationTicks != 0) completed++;
AnimClockDurationTicks = 0;
AnimClockGeneration++;
if (AnimClockDurationTicks != 0) completed++;
AnimClockDurationTicks = 0;
AnimClockGeneration++;
}
if (completed > 0) MarkRetainedMutation();
return completed;
return true;
}
}

View File

@@ -83,7 +83,6 @@ public sealed class VirtualMachine
private bool _sharedProfileShutdownHandled;
private volatile bool _messageSkipEnabled;
private volatile bool _messageSkipServiceActive;
private volatile bool _advSkipServiceEnabled;
private bool _advReadSkipState;
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
private int _messageWindowAlphaSetting;
@@ -138,10 +137,18 @@ public sealed class VirtualMachine
}
}
}
/// <summary>True while a script-owned timed mouse/input callback loop (HISTORY/HIDEWIN family) owns input.</summary>
/// <summary>True while the currently executing script frame owns a timed raw mouse/input callback
/// loop (HISTORY/HIDEWIN/FIELD family). A registered callback in a suspended parent frame is dormant
/// while a nested ADV scene owns the VM and must not suppress that child's ordinary page input.</summary>
public bool IsRawInputCallbackActive
{
get { lock (_interactiveLock) return _rawInputFrame != null; }
get
{
ExecFrame? rawInputFrame;
lock (_interactiveLock) rawInputFrame = _rawInputFrame;
if (rawInputFrame == null) return false;
lock (_debugControlLock) return ReferenceEquals(rawInputFrame, _debugActiveFrame);
}
}
public string? RawInputCallbackScriptName
{
@@ -357,13 +364,13 @@ public sealed class VirtualMachine
} while (Interlocked.CompareExchange(ref field, after, before) != before);
}
/// <summary>Mirror adv_interpreter_tick's bit-0x40 path. The bit is logical action 6 from the
/// <summary>Mirror adv_interpreter_tick's live bit-0x40 poll. The bit is logical action 6 from the
/// process-owned binding map, not a hardcoded Ctrl test; Himegari also binds C and retains the
/// engine's default Backspace binding. The presentation lifecycle gate prevents a held action
/// from leaking into non-ADV script execution.</summary>
/// engine's default Backspace binding. Native polls this physical channel independently of the
/// op-0x19b/0x19c lifecycle used by persistent/read-message Skip.</summary>
private void RefreshPhysicalMessageSkipState()
{
bool active = _advSkipServiceEnabled && (InputBindings.PollActionMask() & 0x40) != 0;
bool active = (InputBindings.PollActionMask() & 0x40) != 0;
_host.SetPhysicalMessageSkipActive(active);
}
@@ -790,7 +797,6 @@ public sealed class VirtualMachine
_autoVoicePending = false;
_messageSkipEnabled = false;
_messageSkipServiceActive = false;
_advSkipServiceEnabled = false;
_advReadSkipState = false;
_advTextStyle = AdvTextStyle.Default;
_systemMenuActionsEnabled = 1;
@@ -799,6 +805,7 @@ public sealed class VirtualMachine
_host.SetMessageSkipActive(false);
_host.SetPhysicalMessageSkipActive(false);
_host.ResetSceneContext();
RefreshPhysicalMessageSkipState();
}
private static Stack<int>[] CreateIntegerStacks()
@@ -2276,22 +2283,19 @@ public sealed class VirtualMachine
Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1;
case "u00414E80":
case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation
_advSkipServiceEnabled = false;
_messageSkipServiceActive = false;
_host.SetMessageSkipActive(false);
_host.SetPhysicalMessageSkipActive(false);
return pc + 1;
case "u00414EC0":
case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry
_advSkipServiceEnabled = true;
_messageSkipServiceActive =
_messageSkipEnabled || _advReadSkipState || _host.IsAdvReadSkipActive;
_host.SetMessageSkipActive(_messageSkipServiceActive);
RefreshPhysicalMessageSkipState();
return pc + 1;
case "get-message-skip": // 0x1c7: persistent Skip or host-supplied Ctrl fast-forward
// The native per-op tick continually re-arms the transient run-state bit while the
// ADV service is enabled. The host channel also carries physical fast-forward input.
// Native persistent state and the independently polled physical action-6 channel both
// re-arm the transient run-state bit consumed by this query.
Write(a[0], _messageSkipServiceActive || _host.IsMessageSkipActive ? 1 : 0); return pc + 1;
case "get-adv-read-skip-state": // 0x1cc: per-message read/click skip service state
case "get-adv-service-state": // compatibility with pre-recovery generated tables

View File

@@ -94,6 +94,7 @@ public sealed class GodotAdvHost : IHost
private volatile bool _modalMovieWaiting;
private volatile bool _modalMovieCancelled;
private GfxState? _foregroundGfx;
private volatile bool _foregroundTransitionWaitBypassed;
private readonly object _screenTransitionLock = new();
private readonly Dictionary<int, IReadOnlyList<RenderObject>> _renderTargetSnapshots = new();
private readonly object _backbufferRangeLock = new();
@@ -662,13 +663,16 @@ public sealed class GodotAdvHost : IHost
{
if (IsTransitionWaiting && _foregroundGfx != null)
{
int completed = _foregroundGfx.CompleteClickSkippableTimedPresentation(_clock.NowMs);
if (completed > 0)
bool accepted = _foregroundGfx.TryCompleteClickSkippableTimedPresentation(
_clock.NowMs, out int completed);
if (accepted)
{
_timeline?.State("transition-forced-complete", new()
_foregroundTransitionWaitBypassed = true;
_timeline?.State("transition-skip-accepted", new()
{
["count"] = completed,
["endpoints_completed"] = completed,
["source"] = source,
["wait_bypassed"] = true,
});
_frameSignal.Set();
return true;
@@ -781,6 +785,7 @@ public sealed class GodotAdvHost : IHost
public void WaitForForegroundTransition(GfxState gfx)
{
_foregroundTransitionWaitBypassed = false;
int started = gfx.StartForegroundTransitions(_clock.NowMs);
bool hasActivePresentation =
gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation();
@@ -790,6 +795,10 @@ public sealed class GodotAdvHost : IHost
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, _clock.NowMs);
IsTransitionWaiting = true;
_timeline?.State("transition-start", new() { ["count"] = started });
// Skip may already have been active before this service boundary. Native ADV script usually
// selects 0x243+0x20c in that case, but applying it here also covers transition helpers that
// enter run-state 0x400 without repeating the script-side query.
if (_messageSkipActive) TryForceActiveTransition("message-skip");
}
bool scriptSuspended = SuspendScriptForPresentation();
try
@@ -797,7 +806,9 @@ public sealed class GodotAdvHost : IHost
RequestSynchronizedPresentation();
if (!hasActivePresentation) return;
int lastBucket = -1;
while ((gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation()) && !_stopping)
while (!_foregroundTransitionWaitBypassed
&& (gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation())
&& !_stopping)
{
var active = gfx.SnapshotForegroundTransitions(_clock.NowMs);
int bucket = active.Count == 0 ? 100 : (int)System.Math.Floor(active[0].Progress * 10);
@@ -812,8 +823,8 @@ public sealed class GodotAdvHost : IHost
}
_frameSignal.WaitOne(50);
}
// The active query becomes false at the exact transition/movie endpoint. Publish that terminal sample
// once so the last visible frame cannot remain fractionally incomplete.
// Publish the natural terminal sample or the forced service-resume sample once before the
// following script burst mutates/releases its retained inputs.
RequestSynchronizedPresentation();
}
finally
@@ -821,6 +832,7 @@ public sealed class GodotAdvHost : IHost
ResumeScriptAfterPresentation(scriptSuspended);
}
IsTransitionWaiting = false;
_foregroundTransitionWaitBypassed = false;
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
_foregroundGfx = null;
_timeline?.State("running", new() { ["transition_complete"] = true });
@@ -1102,13 +1114,10 @@ public sealed class GodotAdvHost : IHost
_frameSignal.Set();
}
// 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);
}
// Native dispatch remains burst-fast between explicit presentation/sleep/input services even when
// message Skip removes those services. Per-op frame pacing turns ordinary skipped setup/cleanup bursts
// into multi-second invisible stalls.
public void FrameYield() { }
// 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;

View File

@@ -265,6 +265,7 @@ public partial class Main : Godot.Control
double sleepScale = 1.0; // --sleep-scale <f>: scale explicit op-0xc8 holds
double speed = 1.0; // --speed <f>: sleeps + retained presentation clocks
long transitionClickMs = -1; // --transition-click-ms <n>: force active transitions after n virtual ms
bool holdMessageSkip = false; // --hold-message-skip: hold native logical action 6 for diagnostics
string? histFile = null; // --trace-histogram <file>: op/call-site execution counts of the REAL run
string? pageMapPath = null; // --page-map <jsonl>: override default build/page-map-SCxxxx.jsonl
for (int i = 0; i < userArgs.Length; i++)
@@ -289,6 +290,7 @@ public partial class Main : Godot.Control
if (userArgs[i] == "--sleep-scale" && i + 1 < userArgs.Length) double.TryParse(userArgs[i + 1], out sleepScale);
if (userArgs[i] == "--speed" && i + 1 < userArgs.Length) double.TryParse(userArgs[i + 1], out speed);
if (userArgs[i] == "--transition-click-ms" && i + 1 < userArgs.Length) long.TryParse(userArgs[i + 1], out transitionClickMs);
if (userArgs[i] == "--hold-message-skip") holdMessageSkip = true;
if (userArgs[i] == "--trace-histogram" && i + 1 < userArgs.Length) histFile = userArgs[i + 1];
if (userArgs[i] == "--page-map" && i + 1 < userArgs.Length) pageMapPath = userArgs[i + 1];
if (userArgs[i] == "--locator-hud") _locatorHudVisible = true;
@@ -457,6 +459,8 @@ public partial class Main : Godot.Control
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
_vm.ExternalGlobals[0x6242d] = 4;
foreach (var (addr, val) in seeds) _vm.Globals[addr] = val; // --seed overrides boot state
if (holdMessageSkip)
_vm.UpdateKeyboardVirtualKeyState(0x11, true); // Ctrl; bindings resolve it to logical action 6.
_vmTask = Task.Run(() =>
{
try { _vm.Run(); }

View File

@@ -50,8 +50,8 @@ INFERRED: dict[int, dict] = {
0x143: dict(name='run-mounted-append-autoruns', category='control', noop=False, confidence='high', source='investigation', summary='() - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller.'),
0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='high', source='investigation', summary='Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.'),
0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'),
0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle."),
0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'),
0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend persistent/read-driven ADV fast-forward while preserving the user's all-message Skip toggle; physically held logical action 6 is independent."),
0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the persistent/read ADV skip service and recompute active fast-forward from all-message Skip or the live read-skip channel; physical action 6 remains independent.'),
0x1a6: dict(name='half-byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write half the resolved string's byte length, using integer truncation."),
0x1a8: dict(name='instruction-marker-noop', category='marker', noop=True, confidence='high', source='investigation', summary="Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."),
0x1b2: dict(name='append-diagnostic-value', category='control', noop=False, confidence='high', source='investigation', summary="Format operand 1 with AGE's generic operand-to-string conversion and append its exact bytes to the context diagnostic accumulator. This does not present or clear the accumulated text."),

View File

@@ -3350,12 +3350,13 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "suspend-adv-skip-service"
category = "input"
summary = "() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle."
summary = "() - suspend persistent/read-driven ADV fast-forward while preserving the user's all-message Skip toggle; physically held logical action 6 is independent."
details = "This lifecycle operation affects persistent/read-driven active Skip only. Physical logical action 6 is process input polled independently by adv_interpreter_tick and remains active while held."
noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
evidence = "Ghidra /v2: op_0x19b_suspend_adv_skip_service@0x416560 clears run-state bit 0x08000000, ctx+0x13dc active message skip, ctx+0x55100 ADV skip-service enable, and the click-cancel phase, but deliberately leaves ctx+0x550fc persistent all-message Skip unchanged. Corpus comments call it `savemesskip`; ADV-end/branch and CALLBACK_LOAD paths use it before leaving ADV presentation."
evidence = "Ghidra /v2: op_0x19b_suspend_adv_skip_service@0x416560 clears run-state bit 0x08000000, ctx+0x13dc active message skip, ctx+0x55100 ADV skip-service enable, and the click-cancel phase, but deliberately leaves ctx+0x550fc persistent all-message Skip unchanged. It does not mutate the input manager; adv_interpreter_tick@0x410fb0 polls logical action 6 (mask bit 0x40) independently of ctx+0x55100. Corpus comments call the opcode `savemesskip`; ADV-end/branch and CALLBACK_LOAD paths use it before leaving ADV presentation."
[[opcode]]
op = 0x19c
@@ -3366,7 +3367,7 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics]
name = "resume-adv-skip-service"
category = "input"
summary = "() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel."
summary = "() - enable the persistent/read ADV skip service and recompute active fast-forward from all-message Skip or the live read-skip channel; physical action 6 remains independent."
noop_headless = false
source = "investigation"
confidence = "high"
@@ -5588,7 +5589,7 @@ abi_source = "kelebek+decode-validated"
name = "mark-frame-yield"
category = "control"
summary = "Set native run-state bit 0x400; in normal ADV playback this is the retained-presentation render/wait/resume boundary."
details = "SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit 0x400 parks the interpreter while gfx_render_frame repeatedly samples finite one-shot object channels and queued surface commands; op 0x224 follows after dirty state clears. When system:EffectSkipOnClick is enabled and animation-service flag bit 0 is clear, logical action 4 clears the service wait, requests the same forced completion as 0x243, renders the terminal retained frame, and consumes the trigger. That request completes ordinary finite color/scale/rotation/translation channels and type-0 surface commands together; op-0x242-detached channels, ambient cycles, and movie masks are excluded. Native trace proves AE001D bind, mode-1 0x203, and 0x202 targets complete in one 5 ms batch with no render, then first compose here."
details = "SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit 0x400 parks the interpreter while gfx_render_frame repeatedly samples finite one-shot object channels and queued surface commands; op 0x224 follows after dirty state clears. When system:EffectSkipOnClick is enabled and animation-service flag bit 0 is clear, logical action 4 is consumed and run-state 0x400 is cleared before the engine requests the same forced completion as 0x243 and renders once. Wait release is independent of endpoint completion: service-flags bit 1 can suppress the force worker, while op-0x242-detached channels, ambient cycles, and movie-backed presentation can continue asynchronously after interpreter resume. With ordinary flags, the request completes finite color/scale/rotation/translation channels and type-0 surface commands together. Native trace proves AE001D bind, mode-1 0x203, and 0x202 targets complete in one 5 ms batch with no render, then first compose here."
noop_headless = false
source = "investigation"
confidence = "high"