Fix ADV transition and nested input lifecycles
This commit is contained in:
@@ -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`;
|
`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.
|
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
|
**Skip pacing regression corrected (2026-07-29).** A later port change incorrectly treated
|
||||||
background VM free-run to the next non-skipped service boundary. That produced whole-scene bursts separated
|
`adv_interpreter_tick`'s one-op dispatch as one rendered frame per opcode only while Skip was active.
|
||||||
by explicit sleeps: visibly an immediate jump, a slow point, then another immediate jump. Native
|
That contradicted the synchronized native trace above: the outer service calls the dispatcher in fast bursts,
|
||||||
`adv_interpreter_tick` still dispatches exactly one opcode per engine tick while persistent Skip removes the
|
and Ctrl measured about 7,738 operand fetches/sec rather than one per display frame. The artificial
|
||||||
ordinary waits. `GodotAdvHost.FrameYield` now consumes one rendered-frame pulse per opcode only while
|
`GodotAdvHost.FrameYield` wait made the setup/cleanup bytecode between skipped fades take seconds even though
|
||||||
message Skip is active. Normal opcode bursts retain the existing run-to-service-boundary model; Skip gains
|
the fades themselves correctly jumped to their endpoints. `FrameYield` is nonblocking again; only explicit
|
||||||
the missing native governor and remains fast without teleporting between blocking points. A regression proves
|
presentation, sleep, and input services own wall-clock time.
|
||||||
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 entry/exit lifecycle.** The script comments `savemesskip` and `loadmesskip` describe a temporary
|
**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
|
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
|
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.
|
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
|
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
|
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
|
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
|
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
|
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.
|
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
|
The live action-6 state now also feeds the ADV fast-forward host channel independently of the
|
||||||
enabled. That transient channel is kept separate from persistent op-`0x88` Skip, so releasing the held key
|
op-`0x19b`/`0x19c` lifecycle gate. Ghidra shows `adv_interpreter_tick` polling the physical action mask before
|
||||||
cannot clear the user's toggle; both channels share the existing text completion, wait advance, voice
|
dispatch on every tick and testing bit `0x40` without consulting `adv_skip_service_enabled`; `0x19b` clears
|
||||||
deferral, and skip cadence. This needs no profile storage, boot seed, or game-specific conditional.
|
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)
|
### ADV right-click/X system-menu path (2026-07-21)
|
||||||
|
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ Port status (2026-07-24): the VM refreshes this state from message:ReadTextSkip
|
|||||||
- **depended on by:** 0x223
|
- **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.
|
- **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
|
## 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.
|
- **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)
|
### 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
|
- **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)
|
### 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
|
- **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.
|
- **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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
and movie masks remain unaffected. Focused exclusions and endpoint regressions pass in the 523-test engine
|
||||||
suite, and the Godot build is warning-free.
|
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,
|
**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
|
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.
|
start/period block—including the preserved raw state for the currently unmodeled second cyclic matrix.
|
||||||
|
|||||||
@@ -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
|
and `colorProgress`, synchronized with the same frame/clock in `--timeline-log`. Parent directories are
|
||||||
created automatically.
|
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.
|
- `--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
|
- `--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
|
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`,
|
ids/names, unknown-fallback `stub` events, virtual time/frame, VM state changes (`running`, `sleep`, `input-wait`,
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ public class ForegroundTransitionTests
|
|||||||
gfx.QueueMovieMaskTransition(new MovieMaskTransitionRequest(
|
gfx.QueueMovieMaskTransition(new MovieMaskTransitionRequest(
|
||||||
11, 45, 10, 1, -184, 0, 800, 600, 0, 0x325e, 0, 1000));
|
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.Equal(1.0, gfx.SnapshotForegroundTransitions(200).Single().Progress);
|
||||||
Assert.False(gfx.TryGet(100)!.OneShotColorEnabled);
|
Assert.False(gfx.TryGet(100)!.OneShotColorEnabled);
|
||||||
@@ -123,9 +124,8 @@ public class ForegroundTransitionTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(1)]
|
[InlineData(1)]
|
||||||
[InlineData(2)]
|
|
||||||
[InlineData(3)]
|
[InlineData(3)]
|
||||||
public void AnimationServiceFlagsSuppressClickCompletion(long flags)
|
public void AnimationServiceFlagBit0RejectsClickCompletion(long flags)
|
||||||
{
|
{
|
||||||
var gfx = new GfxState();
|
var gfx = new GfxState();
|
||||||
gfx.SetSurface(4, 0x25, -1);
|
gfx.SetSurface(4, 0x25, -1);
|
||||||
@@ -133,7 +133,35 @@ public class ForegroundTransitionTests
|
|||||||
gfx.SetAnimatedObjectColorResolved(100, 0, 1000, 0, 0xffffff);
|
gfx.SetAnimatedObjectColorResolved(100, 0, 1000, 0, 0xffffff);
|
||||||
gfx.SetAnimationServiceFlags(flags);
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class InputBindingTests
|
|||||||
Sys4ScriptProvider.Load(Table).RequireByName("SYSTEM4.BIN"), vm.InputBindings);
|
Sys4ScriptProvider.Load(Table).RequireByName("SYSTEM4.BIN"), vm.InputBindings);
|
||||||
|
|
||||||
vm.UpdateKeyboardVirtualKeyState(virtualKey, true);
|
vm.UpdateKeyboardVirtualKeyState(virtualKey, true);
|
||||||
Assert.False(host.PhysicalMessageSkip); // ADV lifecycle is not active yet.
|
Assert.True(host.PhysicalMessageSkip);
|
||||||
|
|
||||||
vm.Run();
|
vm.Run();
|
||||||
|
|
||||||
@@ -98,6 +98,29 @@ public class InputBindingTests
|
|||||||
Assert.False(host.MessageSkip);
|
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]
|
[Fact]
|
||||||
public void ReleasingPhysicalFastForwardDoesNotClearPersistentSkip()
|
public void ReleasingPhysicalFastForwardDoesNotClearPersistentSkip()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,28 @@ using Xunit;
|
|||||||
|
|
||||||
public class WaitForInputTests
|
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
|
// 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.
|
// twice — full handling, no dependency on a real scene's (stubbed) line count.
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -28,4 +50,35 @@ public class WaitForInputTests
|
|||||||
Assert.Equal(new[] { "page one", "page two" }, vm.Emitted.Select(e => e.Text).ToArray());
|
Assert.Equal(new[] { "page one", "page two" }, vm.Emitted.Select(e => e.Text).ToArray());
|
||||||
Assert.Equal("exit", vm.HaltReason);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -934,38 +934,41 @@ public sealed class GfxState
|
|||||||
(o.RotationEnabled && o.RotationPeriodMs > 0)));
|
(o.RotationEnabled && o.RotationPeriodMs > 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Force the finite retained presentation serviced by native run-state bit 0x400 to its
|
/// <summary>Consume the native run-state-0x400 EffectSkipOnClick action and, when permitted, force its
|
||||||
/// endpoint. This is the EffectSkipOnClick path: type-0 surface commands and ordinary finite object
|
/// finite retained presentation to the endpoint. Bit 0 of op 0x24e rejects the action entirely. Bit 1
|
||||||
/// channels complete together, while movie masks, ambient cycles, and op-0x242-detached channels remain
|
/// still allows the service wait to end but suppresses the shared force-complete request. Movie masks,
|
||||||
/// active. Op 0x24e bit 0 disables click skipping; bit 1 suppresses the shared force-complete request.</summary>
|
/// ambient cycles, and op-0x242-detached channels continue asynchronously after the service resumes.</summary>
|
||||||
public int CompleteClickSkippableTimedPresentation(long nowMs)
|
public bool TryCompleteClickSkippableTimedPresentation(long nowMs, out int completed)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if ((AnimationServiceFlags & 3) != 0) return 0;
|
completed = 0;
|
||||||
|
if ((AnimationServiceFlags & 1) != 0) return false;
|
||||||
|
|
||||||
int completed = 0;
|
if ((AnimationServiceFlags & 2) == 0)
|
||||||
foreach (var t in _surfaceTransitions.Values)
|
|
||||||
{
|
{
|
||||||
if (t.Forced || TransitionProgress(t, nowMs) >= 1.0) continue;
|
foreach (var t in _surfaceTransitions.Values)
|
||||||
t.Forced = true;
|
{
|
||||||
completed++;
|
if (t.Forced || TransitionProgress(t, nowMs) >= 1.0) continue;
|
||||||
}
|
t.Forced = true;
|
||||||
|
completed++;
|
||||||
|
}
|
||||||
|
|
||||||
completed += CountOneShotChannels(_rangeTransform);
|
completed += CountOneShotChannels(_rangeTransform);
|
||||||
foreach (var o in _objects.Values)
|
foreach (var o in _objects.Values)
|
||||||
{
|
{
|
||||||
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
|
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
|
||||||
completed += CountOneShotChannels(o);
|
completed += CountOneShotChannels(o);
|
||||||
}
|
}
|
||||||
ForceCompleteOneShotChannels();
|
ForceCompleteOneShotChannels();
|
||||||
|
|
||||||
if (AnimClockDurationTicks != 0) completed++;
|
if (AnimClockDurationTicks != 0) completed++;
|
||||||
AnimClockDurationTicks = 0;
|
AnimClockDurationTicks = 0;
|
||||||
AnimClockGeneration++;
|
AnimClockGeneration++;
|
||||||
|
}
|
||||||
|
|
||||||
if (completed > 0) MarkRetainedMutation();
|
if (completed > 0) MarkRetainedMutation();
|
||||||
return completed;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ public sealed class VirtualMachine
|
|||||||
private bool _sharedProfileShutdownHandled;
|
private bool _sharedProfileShutdownHandled;
|
||||||
private volatile bool _messageSkipEnabled;
|
private volatile bool _messageSkipEnabled;
|
||||||
private volatile bool _messageSkipServiceActive;
|
private volatile bool _messageSkipServiceActive;
|
||||||
private volatile bool _advSkipServiceEnabled;
|
|
||||||
private bool _advReadSkipState;
|
private bool _advReadSkipState;
|
||||||
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
||||||
private int _messageWindowAlphaSetting;
|
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
|
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
|
public string? RawInputCallbackScriptName
|
||||||
{
|
{
|
||||||
@@ -357,13 +364,13 @@ public sealed class VirtualMachine
|
|||||||
} while (Interlocked.CompareExchange(ref field, after, before) != before);
|
} 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
|
/// 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
|
/// engine's default Backspace binding. Native polls this physical channel independently of the
|
||||||
/// from leaking into non-ADV script execution.</summary>
|
/// op-0x19b/0x19c lifecycle used by persistent/read-message Skip.</summary>
|
||||||
private void RefreshPhysicalMessageSkipState()
|
private void RefreshPhysicalMessageSkipState()
|
||||||
{
|
{
|
||||||
bool active = _advSkipServiceEnabled && (InputBindings.PollActionMask() & 0x40) != 0;
|
bool active = (InputBindings.PollActionMask() & 0x40) != 0;
|
||||||
_host.SetPhysicalMessageSkipActive(active);
|
_host.SetPhysicalMessageSkipActive(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,7 +797,6 @@ public sealed class VirtualMachine
|
|||||||
_autoVoicePending = false;
|
_autoVoicePending = false;
|
||||||
_messageSkipEnabled = false;
|
_messageSkipEnabled = false;
|
||||||
_messageSkipServiceActive = false;
|
_messageSkipServiceActive = false;
|
||||||
_advSkipServiceEnabled = false;
|
|
||||||
_advReadSkipState = false;
|
_advReadSkipState = false;
|
||||||
_advTextStyle = AdvTextStyle.Default;
|
_advTextStyle = AdvTextStyle.Default;
|
||||||
_systemMenuActionsEnabled = 1;
|
_systemMenuActionsEnabled = 1;
|
||||||
@@ -799,6 +805,7 @@ public sealed class VirtualMachine
|
|||||||
_host.SetMessageSkipActive(false);
|
_host.SetMessageSkipActive(false);
|
||||||
_host.SetPhysicalMessageSkipActive(false);
|
_host.SetPhysicalMessageSkipActive(false);
|
||||||
_host.ResetSceneContext();
|
_host.ResetSceneContext();
|
||||||
|
RefreshPhysicalMessageSkipState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stack<int>[] CreateIntegerStacks()
|
private static Stack<int>[] CreateIntegerStacks()
|
||||||
@@ -2276,22 +2283,19 @@ public sealed class VirtualMachine
|
|||||||
Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1;
|
Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1;
|
||||||
case "u00414E80":
|
case "u00414E80":
|
||||||
case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation
|
case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation
|
||||||
_advSkipServiceEnabled = false;
|
|
||||||
_messageSkipServiceActive = false;
|
_messageSkipServiceActive = false;
|
||||||
_host.SetMessageSkipActive(false);
|
_host.SetMessageSkipActive(false);
|
||||||
_host.SetPhysicalMessageSkipActive(false);
|
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "u00414EC0":
|
case "u00414EC0":
|
||||||
case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry
|
case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry
|
||||||
_advSkipServiceEnabled = true;
|
|
||||||
_messageSkipServiceActive =
|
_messageSkipServiceActive =
|
||||||
_messageSkipEnabled || _advReadSkipState || _host.IsAdvReadSkipActive;
|
_messageSkipEnabled || _advReadSkipState || _host.IsAdvReadSkipActive;
|
||||||
_host.SetMessageSkipActive(_messageSkipServiceActive);
|
_host.SetMessageSkipActive(_messageSkipServiceActive);
|
||||||
RefreshPhysicalMessageSkipState();
|
RefreshPhysicalMessageSkipState();
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "get-message-skip": // 0x1c7: persistent Skip or host-supplied Ctrl fast-forward
|
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
|
// Native persistent state and the independently polled physical action-6 channel both
|
||||||
// ADV service is enabled. The host channel also carries physical fast-forward input.
|
// re-arm the transient run-state bit consumed by this query.
|
||||||
Write(a[0], _messageSkipServiceActive || _host.IsMessageSkipActive ? 1 : 0); return pc + 1;
|
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-read-skip-state": // 0x1cc: per-message read/click skip service state
|
||||||
case "get-adv-service-state": // compatibility with pre-recovery generated tables
|
case "get-adv-service-state": // compatibility with pre-recovery generated tables
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private volatile bool _modalMovieWaiting;
|
private volatile bool _modalMovieWaiting;
|
||||||
private volatile bool _modalMovieCancelled;
|
private volatile bool _modalMovieCancelled;
|
||||||
private GfxState? _foregroundGfx;
|
private GfxState? _foregroundGfx;
|
||||||
|
private volatile bool _foregroundTransitionWaitBypassed;
|
||||||
private readonly object _screenTransitionLock = new();
|
private readonly object _screenTransitionLock = new();
|
||||||
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();
|
||||||
@@ -662,13 +663,16 @@ public sealed class GodotAdvHost : IHost
|
|||||||
{
|
{
|
||||||
if (IsTransitionWaiting && _foregroundGfx != null)
|
if (IsTransitionWaiting && _foregroundGfx != null)
|
||||||
{
|
{
|
||||||
int completed = _foregroundGfx.CompleteClickSkippableTimedPresentation(_clock.NowMs);
|
bool accepted = _foregroundGfx.TryCompleteClickSkippableTimedPresentation(
|
||||||
if (completed > 0)
|
_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,
|
["source"] = source,
|
||||||
|
["wait_bypassed"] = true,
|
||||||
});
|
});
|
||||||
_frameSignal.Set();
|
_frameSignal.Set();
|
||||||
return true;
|
return true;
|
||||||
@@ -781,6 +785,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
|
|
||||||
public void WaitForForegroundTransition(GfxState gfx)
|
public void WaitForForegroundTransition(GfxState gfx)
|
||||||
{
|
{
|
||||||
|
_foregroundTransitionWaitBypassed = false;
|
||||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||||
bool hasActivePresentation =
|
bool hasActivePresentation =
|
||||||
gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation();
|
gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation();
|
||||||
@@ -790,6 +795,10 @@ public sealed class GodotAdvHost : IHost
|
|||||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, _clock.NowMs);
|
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, _clock.NowMs);
|
||||||
IsTransitionWaiting = true;
|
IsTransitionWaiting = true;
|
||||||
_timeline?.State("transition-start", new() { ["count"] = started });
|
_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();
|
bool scriptSuspended = SuspendScriptForPresentation();
|
||||||
try
|
try
|
||||||
@@ -797,7 +806,9 @@ public sealed class GodotAdvHost : IHost
|
|||||||
RequestSynchronizedPresentation();
|
RequestSynchronizedPresentation();
|
||||||
if (!hasActivePresentation) return;
|
if (!hasActivePresentation) return;
|
||||||
int lastBucket = -1;
|
int lastBucket = -1;
|
||||||
while ((gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation()) && !_stopping)
|
while (!_foregroundTransitionWaitBypassed
|
||||||
|
&& (gfx.HasActiveTimedPresentation(_clock.NowMs) || HasActiveMoviePresentation())
|
||||||
|
&& !_stopping)
|
||||||
{
|
{
|
||||||
var active = gfx.SnapshotForegroundTransitions(_clock.NowMs);
|
var active = gfx.SnapshotForegroundTransitions(_clock.NowMs);
|
||||||
int bucket = active.Count == 0 ? 100 : (int)System.Math.Floor(active[0].Progress * 10);
|
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);
|
_frameSignal.WaitOne(50);
|
||||||
}
|
}
|
||||||
// The active query becomes false at the exact transition/movie endpoint. Publish that terminal sample
|
// Publish the natural terminal sample or the forced service-resume sample once before the
|
||||||
// once so the last visible frame cannot remain fractionally incomplete.
|
// following script burst mutates/releases its retained inputs.
|
||||||
RequestSynchronizedPresentation();
|
RequestSynchronizedPresentation();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -821,6 +832,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
ResumeScriptAfterPresentation(scriptSuspended);
|
ResumeScriptAfterPresentation(scriptSuspended);
|
||||||
}
|
}
|
||||||
IsTransitionWaiting = false;
|
IsTransitionWaiting = false;
|
||||||
|
_foregroundTransitionWaitBypassed = false;
|
||||||
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
System.Threading.Interlocked.Exchange(ref _transitionStartedAtMs, -1);
|
||||||
_foregroundGfx = null;
|
_foregroundGfx = null;
|
||||||
_timeline?.State("running", new() { ["transition_complete"] = true });
|
_timeline?.State("running", new() { ["transition_complete"] = true });
|
||||||
@@ -1102,13 +1114,10 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_frameSignal.Set();
|
_frameSignal.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordinary opcode bursts run to the next service boundary without frame pacing. Persistent message
|
// Native dispatch remains burst-fast between explicit presentation/sleep/input services even when
|
||||||
// Skip removes most of those boundaries, but native adv_interpreter_tick still executes one opcode per
|
// message Skip removes those services. Per-op frame pacing turns ordinary skipped setup/cleanup bursts
|
||||||
// engine tick; retain that cadence here so Skip advances quickly instead of free-running whole scenes.
|
// into multi-second invisible stalls.
|
||||||
public void FrameYield()
|
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.
|
// 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;
|
// Time-based sibling of WaitForInput's suspend. The native op arms a non-blocking main-loop-polled timer;
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ public partial class Main : Godot.Control
|
|||||||
double sleepScale = 1.0; // --sleep-scale <f>: scale explicit op-0xc8 holds
|
double sleepScale = 1.0; // --sleep-scale <f>: scale explicit op-0xc8 holds
|
||||||
double speed = 1.0; // --speed <f>: sleeps + retained presentation clocks
|
double speed = 1.0; // --speed <f>: sleeps + retained presentation clocks
|
||||||
long transitionClickMs = -1; // --transition-click-ms <n>: force active transitions after n virtual ms
|
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? 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
|
string? pageMapPath = null; // --page-map <jsonl>: override default build/page-map-SCxxxx.jsonl
|
||||||
for (int i = 0; i < userArgs.Length; i++)
|
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] == "--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] == "--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] == "--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] == "--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] == "--page-map" && i + 1 < userArgs.Length) pageMapPath = userArgs[i + 1];
|
||||||
if (userArgs[i] == "--locator-hud") _locatorHudVisible = true;
|
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))
|
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
|
||||||
_vm.ExternalGlobals[0x6242d] = 4;
|
_vm.ExternalGlobals[0x6242d] = 4;
|
||||||
foreach (var (addr, val) in seeds) _vm.Globals[addr] = val; // --seed overrides boot state
|
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(() =>
|
_vmTask = Task.Run(() =>
|
||||||
{
|
{
|
||||||
try { _vm.Run(); }
|
try { _vm.Run(); }
|
||||||
|
|||||||
@@ -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.'),
|
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.'),
|
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.'),
|
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."),
|
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 ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'),
|
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."),
|
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."),
|
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."),
|
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."),
|
||||||
|
|||||||
@@ -3350,12 +3350,13 @@ abi_source = "kelebek+decode-validated"
|
|||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "suspend-adv-skip-service"
|
name = "suspend-adv-skip-service"
|
||||||
category = "input"
|
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
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
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]]
|
[[opcode]]
|
||||||
op = 0x19c
|
op = 0x19c
|
||||||
@@ -3366,7 +3367,7 @@ abi_source = "kelebek+decode-validated"
|
|||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "resume-adv-skip-service"
|
name = "resume-adv-skip-service"
|
||||||
category = "input"
|
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
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
@@ -5588,7 +5589,7 @@ abi_source = "kelebek+decode-validated"
|
|||||||
name = "mark-frame-yield"
|
name = "mark-frame-yield"
|
||||||
category = "control"
|
category = "control"
|
||||||
summary = "Set native run-state bit 0x400; in normal ADV playback this is the retained-presentation render/wait/resume boundary."
|
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
|
noop_headless = false
|
||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
|
|||||||
Reference in New Issue
Block a user