feat(port): adopt the measured held-direction repeat rate (F1)
REPEAT_DELAY = 0.402, REPEAT_INTERVAL = 0.134, from docs/re/f1-repeat-measured-via-driver-patch.md -- 12 and 4 frames at the run's achieved 29.87 fps guest rate, converted to seconds because this port does not run at the guest's rate and it is the cadence that was measured. The mechanism has been here since 2026-09-02 and inert on purpose. The instruction it was waiting on is now vindicated in the most awkward way: the draft it refused to ship had 0.40 / 0.20, so the guessed delay was nearly right and the guessed interval was off by 50 %. The half that was wrong would have been protected by the half that was right. The delay and the interval are NOT equally well evidenced, and the code says so at the constants. No physical controller exists in the Decoder's container, so the measurement fed Canary's file driver the SDL driver's own 400/100 ms constants: the 402 ms that came back is the constant that went in, and confirms the instrument. The 133 ms interval against a fed-in 100 ms is the new fact -- the game paces repeats to its own frame consumption. One run; the two-run minimum is not met and the finding says so itself. Also: the prediction that this would turn verify-input's "a held stick is ONE step, not six" red was wrong. It stayed green, because steps() never advances a clock and so had never called repeat_due() at all -- the rate was about to ship into a harness with no coverage of the feature, with a green line that would have been read as coverage. So verify-input gains a `repeat` subject: nothing before the delay, the first repeat on the delay, the steady interval, cadence independent of frame rate (the code claims this in a comment, so it is now asserted), and a direction change restarting the delay. Each asserts THESE numbers, not the shape -- a shape-only check would have passed on 0.40 / 0.20. The control removes the premise, a held direction, and every controllable row inverts. The first-repeat row measures from the arming tick, not from t=0: that tick is the frame the press is handled, which is the origin the finding measures its 12 frames from. Measured from zero it read 0.433 vs 0.402 and the tolerance would have had to be widened to hide a units mismatch. Closes #2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,53 @@ func steps(values: Array, latched: bool) -> int:
|
||||
func nav(values: Array) -> int:
|
||||
return steps(values, mode != "control")
|
||||
|
||||
## Hold a direction through the REAL latch, then tick `repeat_due()` and report
|
||||
## the time of every repeat it produces.
|
||||
##
|
||||
## The press edge is fed through `accepts()` rather than poked into the latch,
|
||||
## so this exercises the same path the port does -- `held_direction()` reads
|
||||
## that latch first. Under `--control` the hold is simply not made: the rate is
|
||||
## a `const` and cannot be removed at runtime, so what the control removes is
|
||||
## the PREMISE (a direction being held), and every count must go to zero.
|
||||
func hold_and_tick(seconds: float, delta: float) -> Array[float]:
|
||||
var pad := Gamepad.new()
|
||||
if mode != "control":
|
||||
pad.accepts(deflect(0.92))
|
||||
var t := 0.0
|
||||
var out: Array[float] = []
|
||||
while t < seconds:
|
||||
t += delta
|
||||
if pad.repeat_due(delta) != 0:
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
## Hold one way past the delay, reverse, and report how long until the first
|
||||
## repeat in the NEW direction. Inheriting the old cadence would show up here as
|
||||
## a time far below `REPEAT_DELAY`.
|
||||
func reversal_delay(delta: float) -> float:
|
||||
var pad := Gamepad.new()
|
||||
if mode != "control":
|
||||
pad.accepts(deflect(0.92))
|
||||
var t := 0.0
|
||||
while t < 1.0:
|
||||
t += delta
|
||||
pad.repeat_due(delta)
|
||||
pad.accepts(deflect(0.0))
|
||||
if mode != "control":
|
||||
pad.accepts(deflect(-0.92))
|
||||
t = 0.0
|
||||
while t < 2.0:
|
||||
t += delta
|
||||
if pad.repeat_due(delta) != 0:
|
||||
return t
|
||||
return -1.0
|
||||
|
||||
func deflect(v: float) -> InputEventJoypadMotion:
|
||||
var e := InputEventJoypadMotion.new()
|
||||
e.axis = JOY_AXIS_LEFT_Y
|
||||
e.axis_value = v
|
||||
return e
|
||||
|
||||
func _init() -> void:
|
||||
# The control removes the repair. Everything else runs with it applied.
|
||||
if mode != "control":
|
||||
@@ -201,6 +248,76 @@ func _init() -> void:
|
||||
passed += 1
|
||||
ok("d-pad presses are not latched", "negative", passed == 3, "%d of 3" % passed)
|
||||
|
||||
# ── 4. subject `repeat` -- a held direction repeats at the MEASURED rate ──
|
||||
#
|
||||
# 🔴 THIS SECTION EXISTS BECAUSE A PREDICTION IN `gamepad.gd` WAS WRONG.
|
||||
# That file said adopting the rate would turn "a held stick is ONE step, not
|
||||
# six" red, and warned that the row would read as a regression. Measured on
|
||||
# adoption day: it stays green, because `steps()` never advances a clock and
|
||||
# so has never called `repeat_due()` at all. The warning was reasoned, not
|
||||
# run -- and the real consequence is worse than the one predicted. The rate
|
||||
# shipped into a harness with **no coverage of the feature whatsoever**.
|
||||
#
|
||||
# The rows below are that coverage. They assert the two numbers from
|
||||
# `docs/re/f1-repeat-measured-via-driver-patch.md`, not the shape alone: a
|
||||
# test that only checked "it repeats eventually" would pass on any constant
|
||||
# and would have passed on the 0.40 / 0.20 guess this port deliberately
|
||||
# refused to ship.
|
||||
var FRAME := 1.0 / 60.0
|
||||
|
||||
# Hold the stick by pushing it through the same latch the port uses, then
|
||||
# tick. `held_direction()` reads the latch, so this is the real path.
|
||||
var timeline := hold_and_tick(2.0, FRAME)
|
||||
# ⚠️ MEASURED FROM THE ARMING TICK, NOT FROM t=0, and the difference is a
|
||||
# whole frame. `repeat_due()`'s first call only latches the direction and
|
||||
# returns 0; the clock accumulates from the call after it. In the port that
|
||||
# first call happens on the frame the press is handled -- the frame that
|
||||
# produced the press-triggered step -- and the finding measures its 12
|
||||
# frames "from the press-triggered step to the first repeat". So the arming
|
||||
# tick is the press step, and subtracting it is what puts the harness and
|
||||
# the finding on the same origin. Without this the row read 0.433 vs 0.402
|
||||
# and the tolerance would have had to be widened to hide a units mismatch.
|
||||
var first: float = (timeline[0] - FRAME) if not timeline.is_empty() else -1.0
|
||||
|
||||
ok("nothing repeats before the measured delay", "negative",
|
||||
first >= Gamepad.REPEAT_DELAY,
|
||||
"first repeat %.3fs after the press step, delay is %.3f"
|
||||
% [first, Gamepad.REPEAT_DELAY],
|
||||
"the steady-interval row below")
|
||||
ok("first repeat lands on the measured delay", "repeat",
|
||||
first >= 0.0 and absf(first - Gamepad.REPEAT_DELAY) <= FRAME,
|
||||
"%.3fs vs %.3f (±one frame)" % [first, Gamepad.REPEAT_DELAY])
|
||||
|
||||
var gaps: Array[float] = []
|
||||
for i in range(1, timeline.size()):
|
||||
gaps.append(timeline[i] - timeline[i - 1])
|
||||
var mean := 0.0
|
||||
for g in gaps:
|
||||
mean += g
|
||||
mean = mean / gaps.size() if not gaps.is_empty() else -1.0
|
||||
ok("steady interval is the measured 0.134s", "repeat",
|
||||
not gaps.is_empty() and absf(mean - Gamepad.REPEAT_INTERVAL) <= FRAME,
|
||||
"mean %.3fs over %d gap(s) vs %.3f" % [mean, gaps.size(), Gamepad.REPEAT_INTERVAL])
|
||||
|
||||
# `repeat_due()` subtracts the interval rather than resetting the clock,
|
||||
# with the stated reason "at 140 fps and at 30 fps the same number of steps
|
||||
# happen per second". That is a claim about the code, so it is asserted
|
||||
# rather than believed.
|
||||
var at30 := hold_and_tick(2.0, 1.0 / 30.0).size()
|
||||
var at240 := hold_and_tick(2.0, 1.0 / 240.0).size()
|
||||
# `at30 > 0` matters: with nothing held both counts are 0 and "they agree"
|
||||
# would be a green line for a mechanism that never ran -- the control caught
|
||||
# exactly that, so the count is asserted as well as the agreement.
|
||||
ok("the cadence does not drift with frame rate", "repeat",
|
||||
at30 > 0 and absf(at30 - at240) <= 1,
|
||||
"%d steps at 30fps, %d at 240fps" % [at30, at240])
|
||||
|
||||
# A direction change must restart the delay, not inherit the old cadence --
|
||||
# otherwise flicking the other way mid-repeat steps instantly.
|
||||
ok("a direction change restarts the delay", "repeat",
|
||||
reversal_delay(FRAME) >= Gamepad.REPEAT_DELAY,
|
||||
"%.3fs after the reversal" % reversal_delay(FRAME))
|
||||
|
||||
if ran == 0:
|
||||
print("🔴 no check ran -- the harness asserted nothing")
|
||||
quit(2)
|
||||
|
||||
Reference in New Issue
Block a user