Merge pull request 'feat(port): adopt the measured held-direction repeat rate (F1)' (#27) from feat/f1-held-repeat into main
Reviewed-on: #27
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