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:
@@ -1,7 +1,10 @@
|
||||
# F1 — the menu repeats on a held direction: mechanism shipped, **rate deliberately not**
|
||||
# F1 — the menu repeats on a held direction: mechanism shipped, **and the rate adopted**
|
||||
|
||||
**Status:** ✅ mechanism implemented and wired. 🔴 **inert on purpose** — it does
|
||||
nothing until a measured repeat rate exists. Written 2026-09-02 by the Port.
|
||||
**Status:** ✅ mechanism implemented and wired, 2026-09-02. ✅ **rate adopted
|
||||
2026-09-13** from `docs/re/f1-repeat-measured-via-driver-patch.md` —
|
||||
`REPEAT_DELAY = 0.402`, `REPEAT_INTERVAL = 0.134`. It ran inert for eleven days
|
||||
and that was the right state; this page keeps the inert-era reasoning because it
|
||||
is why the two numbers can be trusted now.
|
||||
|
||||
## What was reported
|
||||
|
||||
@@ -57,42 +60,136 @@ A repeat that could fire during a movie or mid-transition would be a **second,
|
||||
subtly different input path**, and the first thing this port learned about input
|
||||
is that a second path is where the defect hides.
|
||||
|
||||
## 🔴 And the rate is not shipped
|
||||
## ✅ The rate, and how far each half of it reaches
|
||||
|
||||
An earlier draft of this change had `REPEAT_DELAY = 0.40` and
|
||||
`REPEAT_INTERVAL = 0.20`, with a paragraph explaining that they were authored.
|
||||
**They were removed rather than commented out**, on an explicit instruction:
|
||||
An earlier draft had `REPEAT_DELAY = 0.40` and `REPEAT_INTERVAL = 0.20` with a
|
||||
paragraph explaining that they were authored. They were removed rather than
|
||||
commented out, on an explicit instruction:
|
||||
|
||||
> *"Take the RATE from the Decoder — do NOT ship a placeholder interval. An
|
||||
> invented rate here is indistinguishable from a measured one later, and this is
|
||||
> the exact field where that already cost us."*
|
||||
|
||||
The instruction is right and the draft was the named failure mode: the
|
||||
explanation would have merged, the numbers would have felt roughly right, and
|
||||
nothing downstream could have separated them from a measurement. `REPEAT_DELAY`
|
||||
is `-1.0`; `repeat_due()` returns 0 while `repeat_rate_known()` is false.
|
||||
📌 **The measurement has now landed, and it vindicates the instruction in the
|
||||
most awkward possible way: the guessed delay was nearly right and the guessed
|
||||
interval was off by 50 %.** 0.40 against a measured 0.402; 0.20 against a
|
||||
measured 0.134. Had both shipped, the half that was wrong would have been
|
||||
protected by the half that was right.
|
||||
|
||||
**One thing about the rate IS measured, and it narrows the question.** The game
|
||||
digitises the left stick to four direction bits at 61 % deflection, so it cannot
|
||||
see deflection magnitude at all — the repeat it drives *cannot* be
|
||||
faster-the-harder-you-push. That excludes the one competing model, so only two
|
||||
constants are open and a single measurement closes both.
|
||||
The Decoder measured, in Canary at an achieved 29.87 fps guest rate, **12 frames**
|
||||
from the press-triggered step to the first repeat and **4 frames** per step after
|
||||
it. Converted to seconds here because this port does not run at the guest's rate
|
||||
and it is the cadence that was measured: 12 / 29.87 = **0.402**, 4 / 29.87 =
|
||||
**0.134**.
|
||||
|
||||
## ⚠️ Adopting the rate breaks a green check, for the right reason
|
||||
### ⚠️ The two numbers are not equally well evidenced
|
||||
|
||||
`tools/port/verify-input` asserts *"a held stick is ONE step, not six"*. That row
|
||||
passes today **because the feature is inert**, i.e. it asserts the absence of the
|
||||
repeat. When a rate is adopted a held stick should produce further steps and that
|
||||
row will go red.
|
||||
No physical controller exists in the Decoder's container. The measurement was
|
||||
taken by patching Canary's `--hid=file` driver to emit Keystroke `REPEAT` at the
|
||||
**SDL driver's own** 400 ms / 100 ms constants:
|
||||
|
||||
It is not wrong and it should not be deleted in a hurry: it was written for the
|
||||
2026-09-01 jitter defect, so it will *look* like that bug returning. It has to be
|
||||
re-stated as "one step per deflection **plus** the measured repeat", with the
|
||||
jitter case still covered inside the delay window.
|
||||
* the **delay** came back as 402 ms — to within the frame quantum, *the constant
|
||||
that was fed in*. It confirms the instrument, not the game;
|
||||
* the **interval** came back as 133 ms against a fed-in 100 ms. That gap is the
|
||||
genuinely new fact: the game paces repeats to its own frame consumption rather
|
||||
than to the event stream.
|
||||
|
||||
So the interval is what the game does; the delay is what Xenia's SDL driver does,
|
||||
and the game was not observed to disagree. **One run** — the corpus's own two-run
|
||||
minimum is not met, and the source page says so itself.
|
||||
|
||||
**One thing about the rate was already measured, and it narrowed the question.**
|
||||
The game digitises the left stick to four direction bits at 61 % deflection, so
|
||||
it cannot see deflection magnitude at all — the repeat it drives *cannot* be
|
||||
faster-the-harder-you-push. That excluded the one competing model, so only two
|
||||
constants were ever open and a single measurement closed both.
|
||||
|
||||
## 🔴 Adopting the rate was predicted to break a green check. It did not, and that was worse
|
||||
|
||||
This page and `gamepad.gd` both said that `verify-input`'s row *"a held stick is
|
||||
ONE step, not six"* asserts the **absence** of the repeat, and would go red on
|
||||
adoption — looking like the 2026-09-01 jitter defect returning.
|
||||
|
||||
**Run on adoption day: the row stayed green.** `steps()` feeds axis values through
|
||||
the latch and never advances a clock, so it had never called `repeat_due()` at
|
||||
all. The row tests the *latch*, which the repeat does not touch. The prediction
|
||||
was reasoned rather than run.
|
||||
|
||||
What it hid is the real problem: the rate was about to ship into a harness with
|
||||
**no coverage of this feature whatsoever**, and that green line would have been
|
||||
read as coverage of it.
|
||||
|
||||
The fix was not to change that row. `verify-input` gains a `repeat` subject that
|
||||
holds a direction through the same latch and ticks `repeat_due()`, asserting
|
||||
**these two numbers** — a shape-only check ("it repeats eventually") would have
|
||||
passed on the 0.40 / 0.20 guess this port refused to ship. Five rows: 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 asserted),
|
||||
and a direction change restarting the delay.
|
||||
|
||||
⚠️ **The origin is one frame, and it is not a tolerance.** `repeat_due()`'s first
|
||||
call only latches the direction; the clock accumulates from the call after it. In
|
||||
the port that first call is the frame the press is handled — the frame that
|
||||
produced the press-triggered step — which is the origin the finding measures its
|
||||
12 frames from. Measured from `t = 0` the harness read 0.433 against 0.402 and
|
||||
the tolerance would have had to be widened to hide a units mismatch.
|
||||
|
||||
## 🔴 It did not work on a real controller, and every instrument here said it did
|
||||
|
||||
**Reported 2026-09-13, by a human holding a real stick: one step, then nothing.**
|
||||
|
||||
`held_direction()`'s own comment said *"Polled at the DEVICE, never through
|
||||
`Input.is_action_pressed`"*. It was not. For the stick it read `_latched` — a
|
||||
reconstruction of the stick's position from the **event history** — and only the
|
||||
d-pad and keyboard branches actually polled anything.
|
||||
|
||||
`_latched` changes only when an event arrives. A stick held perfectly still
|
||||
sends nothing, so the reconstruction is only as good as the last event seen, and
|
||||
any single event reading below `RELEASE` — a spring settling, a deadzone-shaped
|
||||
value, a driver emitting a zero on focus change — clears it with nothing
|
||||
afterwards to set it back. From then on the port believes the stick is centred
|
||||
while the player is holding it. That is exactly "it moves one item and stops".
|
||||
|
||||
### Why nothing here caught it
|
||||
|
||||
Every instrument in this repo **supplies the input it then measures**:
|
||||
|
||||
| instrument | what it feeds |
|
||||
|---|---|
|
||||
| `verify-input`'s `repeat` rows | calls `repeat_due()` directly, after setting the latch through `accepts()` |
|
||||
| `--script=up,down,accept` | `InputEventAction` — bypasses the input map entirely |
|
||||
| `--script=hold:down:2.0` | injects one `InputEventJoypadMotion` |
|
||||
|
||||
All three agreed with each other and none of them agreed with the controller,
|
||||
because none of them read a device. This is the same shape as the 2026-09-01
|
||||
report that opened `gamepad.gd`: *a synthetic-input test asserts the code after
|
||||
the input map, never the input map itself* — one level deeper, and it caught us
|
||||
again with the lesson already written down.
|
||||
|
||||
### What changed
|
||||
|
||||
`held_direction()` now polls `Input.get_joy_axis()` against the game's own 0.61,
|
||||
which is what its comment always meant. The latch survives as a **fallback for
|
||||
injected events**, because `Input.parse_input_event()` does move `get_joy_axis()`
|
||||
but the harness must keep working if that ever changes.
|
||||
|
||||
And `--input-probe` prints what the devices report, on change:
|
||||
|
||||
```
|
||||
probe: [0] Generic X-Box pad Y=+1.000 X=+0.000 dpad=-- latched=1 held_direction=1 (ENTER=0.61 RELEASE=0.40)
|
||||
```
|
||||
|
||||
⚠️ **This fix is not verified.** It is a defect that matches the symptom exactly,
|
||||
found by reading, and the only instrument that can confirm it is a human holding
|
||||
a stick. The probe exists so that the answer is a measurement either way.
|
||||
|
||||
## What this does not claim
|
||||
|
||||
* That the repeat feels right. It cannot — it does not run.
|
||||
* That the repeat feels right. It runs now, and **only a human can answer that**:
|
||||
0.134 s is ~7.5 steps a second, and the play-test that opened this asked for
|
||||
*"slow enough to see which item is selected"*. If it reads as too fast, the
|
||||
100 ms constant is Xenia's and not the game's, and no capture in that
|
||||
container would have revealed it.
|
||||
* Any rate, or any bound on one. "Medium pace" is a direction, not a number, and
|
||||
it is not recorded anywhere as data.
|
||||
* That the d-pad and the stick repeat at the *same* rate. Both repeat; nobody
|
||||
|
||||
@@ -549,6 +549,7 @@ func _process(delta: float) -> void:
|
||||
view.queue_redraw()
|
||||
_overlay_process(delta)
|
||||
_menu_repeat(delta)
|
||||
_input_probe()
|
||||
|
||||
if _player != null:
|
||||
# `--skip-at=SECONDS` presses (A) at a wall-clock moment DURING a movie,
|
||||
@@ -962,6 +963,25 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
## a screen with no menu would be a second, subtly different input path, and the
|
||||
## first thing this port learned about input is that a second path is where the
|
||||
## defect hides.
|
||||
## `--input-probe` prints what the DEVICES report, whenever it changes.
|
||||
##
|
||||
## 🔴 WHY IT EXISTS. On 2026-09-13 a human held a real stick and the cursor moved
|
||||
## once. Every instrument here disagreed with them and agreed with each other —
|
||||
## `verify-input` ticks `repeat_due()` directly, `--script=hold:` injects its own
|
||||
## axis event — because all of them SUPPLY the input they then measure. None
|
||||
## could see a controller. This is the one line that reads the device and says
|
||||
## what it says, so the next report of this shape starts from a measurement.
|
||||
var _probe_last := ""
|
||||
|
||||
func _input_probe() -> void:
|
||||
if not _args().has("input-probe"):
|
||||
return
|
||||
var line := _pad.probe_line()
|
||||
if line != _probe_last:
|
||||
_probe_last = line
|
||||
print("probe: %s" % line)
|
||||
|
||||
|
||||
func _menu_repeat(delta: float) -> void:
|
||||
var step := _pad.repeat_due(delta)
|
||||
if step == 0:
|
||||
@@ -1349,6 +1369,45 @@ func _run_script() -> void:
|
||||
var until := Time.get_ticks_msec() + int(secs * 1000.0)
|
||||
while Time.get_ticks_msec() < until:
|
||||
await get_tree().process_frame
|
||||
elif token.begins_with("hold:"):
|
||||
# 🔴 THE ONE THING `--script` STRUCTURALLY COULD NOT DO, AND THE ONE
|
||||
# THING F1 IS ABOUT. Every other step sends an `InputEventAction`,
|
||||
# which BYPASSES the input map and is not an analog axis — the
|
||||
# lesson `gamepad.gd` opens with. A held direction is exactly an
|
||||
# analog axis that is not re-sent, so nothing in this harness could
|
||||
# ever observe the repeat, and the first report that it does not
|
||||
# work came from a human with a real controller.
|
||||
#
|
||||
# This injects ONE real `InputEventJoypadMotion` at full deflection
|
||||
# and then sends NOTHING for the duration — which is what a stick
|
||||
# held still actually looks like — logging every focus change.
|
||||
var bits := token.split(":")
|
||||
var secs2 := float(bits[2]) if bits.size() > 2 else 1.0
|
||||
var dir := -1.0 if bits[1] == "up" else 1.0
|
||||
print("script[%d] hold %s for %.2f s at %.2f s"
|
||||
% [i + 1, bits[1], secs2, _elapsed])
|
||||
var m := InputEventJoypadMotion.new()
|
||||
m.axis = JOY_AXIS_LEFT_Y
|
||||
m.axis_value = dir
|
||||
Input.parse_input_event(m)
|
||||
var seen := _focus_label(view.focused_id)
|
||||
var moves := 0
|
||||
var t0 := _elapsed
|
||||
while _elapsed - t0 < secs2:
|
||||
await get_tree().process_frame
|
||||
var now := _focus_label(view.focused_id)
|
||||
if now != seen:
|
||||
moves += 1
|
||||
print(" hold move %d -> %s at %+.3f s"
|
||||
% [moves, now, _elapsed - t0])
|
||||
seen = now
|
||||
# Release, so the latch re-arms for whatever follows.
|
||||
var rel := InputEventJoypadMotion.new()
|
||||
rel.axis = JOY_AXIS_LEFT_Y
|
||||
rel.axis_value = 0.0
|
||||
Input.parse_input_event(rel)
|
||||
print(" hold produced %d move(s) in %.2f s (held_direction=%d at end)"
|
||||
% [moves, secs2, _pad.held_direction()])
|
||||
elif token == "wait":
|
||||
pass
|
||||
elif SCRIPT_ACTIONS.has(token):
|
||||
|
||||
@@ -100,17 +100,43 @@ const RELEASE := 0.4
|
||||
## too."* So the FACT covers both input devices, which is why `held_direction()`
|
||||
## polls the pad and the keyboard and not just the stick.
|
||||
##
|
||||
## 🔴 **THE RATE IS DELIBERATELY UNSET, AND THE REPEAT DOES NOT RUN UNTIL IT IS
|
||||
## MEASURED.** The instruction is explicit: *"Take the RATE from the Decoder — do
|
||||
## NOT ship a placeholder interval. An invented rate here is indistinguishable
|
||||
## from a measured one later, and this is the exact field where that already cost
|
||||
## us."*
|
||||
## ✅ **THE RATE IS NOW MEASURED, 2026-09-12, and adopted here.**
|
||||
## `docs/re/f1-repeat-measured-via-driver-patch.md`, with the per-transition
|
||||
## reference data in `docs/re/data/f1-repeat-cursor-transitions.tsv`.
|
||||
##
|
||||
## An earlier draft of this file had 0.40 / 0.20 with a paragraph explaining that
|
||||
## they were authored. **That is precisely the failure mode named above** — the
|
||||
## explanation would have been merged, the numbers would have felt roughly right,
|
||||
## and nothing afterwards could distinguish them from a measurement. They are
|
||||
## removed rather than commented out.
|
||||
## ~~THE RATE IS DELIBERATELY UNSET, AND THE REPEAT DOES NOT RUN UNTIL IT IS
|
||||
## MEASURED.~~ It ran unset for eleven days and that was the right state; the
|
||||
## paragraph is struck through rather than deleted because the reason it gave is
|
||||
## the reason these two numbers can be trusted now. An earlier draft had
|
||||
## **0.40 / 0.20** with a note saying they were authored — and 0.40 would have
|
||||
## looked vindicated today while 0.20 was off by 50 %. That is exactly why an
|
||||
## explained guess is worse than none: half of it would have been right.
|
||||
##
|
||||
## ## Where the two numbers come from, and how far they reach
|
||||
##
|
||||
## Measured in Canary at an achieved **29.87 fps** guest rate: **12 frames**
|
||||
## from the press-triggered step to the first repeat, then **4 frames** per step
|
||||
## (13 of 15 gaps; 3 frames for the other 2). Converted to seconds here, not
|
||||
## frames, because this port does not run at the guest's rate and it is the
|
||||
## *cadence* that was measured — 12 / 29.87 = 0.402, 4 / 29.87 = 0.134.
|
||||
##
|
||||
## ⚠️ **THE DELAY IS WEAKER EVIDENCE THAN THE INTERVAL, and they should not be
|
||||
## trusted equally.** No physical controller exists in that container, so the
|
||||
## measurement was taken by patching Canary's `--hid=file` driver to emit
|
||||
## Keystroke `REPEAT` at the SDL driver's own 400 ms / 100 ms constants. The
|
||||
## 402 ms that came back is, to within the frame quantum, **the constant that
|
||||
## was fed in** — it confirms the instrument, not the game. The 133 ms interval
|
||||
## is the genuinely new fact: the driver was fed 100 ms and the cursor moved
|
||||
## every 133, so the game paces repeats to its own frame consumption rather
|
||||
## than to the event stream.
|
||||
##
|
||||
## So: the interval is what the game does. The delay is what Xenia's SDL driver
|
||||
## does, and the game was not observed to disagree with it. If a capture through
|
||||
## a real controller ever contradicts 0.402, that is the number to move.
|
||||
##
|
||||
## 📌 One run. The corpus's own two-run minimum is **not met** — the source page
|
||||
## says so itself, and this comment repeats it rather than letting the constant
|
||||
## look firmer at the call site than it does at the finding.
|
||||
##
|
||||
## 📌 **A constant interval is the right SHAPE, and that part IS measured.** The
|
||||
## game digitises the left stick to four direction bits at 61 % deflection
|
||||
@@ -118,20 +144,27 @@ const RELEASE := 0.4
|
||||
## drives cannot be faster-the-harder-you-push. That excludes the one competing
|
||||
## model, so only the two constants are open, and one measurement closes both.
|
||||
##
|
||||
## ⚠️ **TO ADOPT, TWO THINGS CHANGE, NOT ONE.** Set both constants to the
|
||||
## measured seconds — and update `tools/port/verify-input`, whose row *"a held
|
||||
## stick is ONE step, not six"* currently asserts **the absence of this
|
||||
## feature**. It passes today because the repeat is inert; the moment a rate is
|
||||
## adopted a held stick SHOULD produce further steps, and that green row would
|
||||
## go red for the right reason and be read as a regression.
|
||||
## ⚠️ ~~**TO ADOPT, TWO THINGS CHANGE, NOT ONE.** … that green row would go red
|
||||
## for the right reason and be read as a regression.~~
|
||||
##
|
||||
## 📌 That row is not wrong. A check written against today's behaviour becomes an
|
||||
## assertion that the behaviour never changes, and this one has the additional
|
||||
## trap of looking like a bug-fix regression test — it was written for the
|
||||
## jitter defect, and the repeat is not that defect returning.
|
||||
## `pad-repeat` in `BLOCKED.md` carries the request.
|
||||
const REPEAT_DELAY := -1.0
|
||||
const REPEAT_INTERVAL := -1.0
|
||||
## 🔴 **RUN ON ADOPTION DAY: THE ROW STAYED GREEN, AND THAT IS WORSE.**
|
||||
## `verify-input`'s `steps()` never advances a clock, so it had never called
|
||||
## `repeat_due()` at all — the row it warned about tests the *latch*, which the
|
||||
## repeat does not touch. The prediction was reasoned rather than run, and what
|
||||
## it hid is the real problem: the rate was about to ship into a harness with
|
||||
## **no coverage of this feature whatsoever**, and the green line would have
|
||||
## been read as coverage.
|
||||
##
|
||||
## The fix was not to change that row. It was to add a `repeat` subject that
|
||||
## holds a direction through the same latch and ticks `repeat_due()`, asserting
|
||||
## **these two numbers** rather than "it repeats eventually" — a shape-only
|
||||
## check would have passed on the 0.40 / 0.20 guess this file refused to ship.
|
||||
##
|
||||
## 📌 The original point survives intact and is worth keeping: a check written
|
||||
## against today's behaviour becomes an assertion that the behaviour never
|
||||
## changes. It was simply aimed at the wrong row.
|
||||
const REPEAT_DELAY := 0.402
|
||||
const REPEAT_INTERVAL := 0.134
|
||||
|
||||
|
||||
## Whether a measured repeat rate has been adopted. Until it has, the port keeps
|
||||
@@ -219,16 +252,37 @@ func accepts(event: InputEvent) -> bool:
|
||||
## exclude — so the repeat would contradict the threshold on the same stick.
|
||||
## That is the input-map lesson again: assert the device, not the layer above it.
|
||||
func held_direction() -> int:
|
||||
# The stick, from the latch `accepts()` already maintains, so the repeat and
|
||||
# the first step read one state and cannot disagree about hysteresis.
|
||||
var stick := int(_latched.get(JOY_AXIS_LEFT_Y, 0))
|
||||
if stick != 0:
|
||||
return stick
|
||||
# 🔴 THE STICK IS NOW ACTUALLY POLLED. It used to read `_latched` — a
|
||||
# reconstruction of the stick's position from the event history — while the
|
||||
# comment above said "polled at the DEVICE". A human holding a real stick
|
||||
# got exactly one step and no repeat (2026-09-13), and the harness could not
|
||||
# see it, because the harness fed the same events the latch was built from.
|
||||
#
|
||||
# `_latched` only changes when an event ARRIVES. A stick held perfectly
|
||||
# still sends nothing, and any one event that reads below `RELEASE` — a
|
||||
# spring settling, a deadzone-shaped value, a driver that emits a zero on
|
||||
# focus change — clears it with no event afterwards to set it back. The
|
||||
# position was then wrong until the player moved the stick again, which is
|
||||
# indistinguishable from "the repeat does not work".
|
||||
#
|
||||
# `get_joy_axis()` is the position itself, tested against the game's own
|
||||
# 0.61, which is what the paragraph above always meant.
|
||||
for device in Input.get_connected_joypads():
|
||||
var v := Input.get_joy_axis(device, JOY_AXIS_LEFT_Y)
|
||||
if v >= ENTER:
|
||||
return 1
|
||||
if v <= -ENTER:
|
||||
return -1
|
||||
if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_UP):
|
||||
return -1
|
||||
if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_DOWN):
|
||||
return 1
|
||||
# The latch, for events that were INJECTED rather than read off a device:
|
||||
# `Input.parse_input_event()` does not move `get_joy_axis()`, so the script
|
||||
# harness and `verify-input` would otherwise test nothing at all here.
|
||||
var stick := int(_latched.get(JOY_AXIS_LEFT_Y, 0))
|
||||
if stick != 0:
|
||||
return stick
|
||||
if Input.is_key_pressed(KEY_UP):
|
||||
return -1
|
||||
if Input.is_key_pressed(KEY_DOWN):
|
||||
@@ -236,6 +290,29 @@ func held_direction() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
## What the devices actually report, for a human to read while holding a stick.
|
||||
##
|
||||
## The 2026-09-13 report — "I hold it down, it moves one item and stops" — could
|
||||
## not be diagnosed from here: every instrument in this repo feeds its own
|
||||
## events, so all of them agreed with each other and none of them agreed with
|
||||
## the controller. This prints the raw state so the next such report starts from
|
||||
## a measurement instead of a guess.
|
||||
func probe_line() -> String:
|
||||
var parts := PackedStringArray()
|
||||
for device in Input.get_connected_joypads():
|
||||
parts.append("[%d] %s Y=%+.3f X=%+.3f dpad=%s%s" % [
|
||||
device, Input.get_joy_name(device),
|
||||
Input.get_joy_axis(device, JOY_AXIS_LEFT_Y),
|
||||
Input.get_joy_axis(device, JOY_AXIS_LEFT_X),
|
||||
"U" if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_UP) else "-",
|
||||
"D" if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_DOWN) else "-"])
|
||||
if parts.is_empty():
|
||||
parts.append("no joypad")
|
||||
return "%s latched=%d held_direction=%d (ENTER=%.2f RELEASE=%.2f)" % [
|
||||
" ".join(parts), int(_latched.get(JOY_AXIS_LEFT_Y, 0)), held_direction(),
|
||||
ENTER, RELEASE]
|
||||
|
||||
|
||||
## One repeat step, or 0. Call once per frame with the frame's delta.
|
||||
##
|
||||
## The FIRST step is not this function's: it comes from the event edge 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