Input.key_pressed / key_released never fire (edge detection broken) — key_down works #87

Closed
opened 2026-09-02 06:11:02 +02:00 by orkun · 1 comment
Owner

Input.key_pressed(key:) never returns true even when the key transitions from up to down; Input.key_down reports the key correctly. Reproduced deterministically headless (the device layer builds in_held from the polled stdin char):

handler In phase Input {
  Input.poll()
  var d = 0; var p = 0
  if Input.key_down(key: 'k')    { d = 1 }
  if Input.key_pressed(key: 'k') { p = 1 }
  print(frame*100 + d*10 + p)
}
# stdin "xxkkkkxxzz" (2 chars/frame) prints: 100 210 310 400 500 600
# expected frame 2 = 211 (held AND pressed edge), frame 3 = 310

input_device_commit snapshots in_prev <- in_held before rebuilding in_held, and input_key_pressed is held && !prev, so on paper the edge should appear on the first held frame. It doesn't, so something else is resetting/aliasing in_prev (suspects: in_init() re-allocating sets, or prev being refreshed again after the rebuild). key_released likely shares the fault.

Impact: any game verb bound to a press edge (dash, melee, menu confirm) silently never triggers. Workaround: game-side edge detection with a previous-state flag. Windowed path shares the same code, so this is not headless-only.

`Input.key_pressed(key:)` never returns true even when the key transitions from up to down; `Input.key_down` reports the key correctly. Reproduced deterministically headless (the device layer builds `in_held` from the polled stdin char): ``` handler In phase Input { Input.poll() var d = 0; var p = 0 if Input.key_down(key: 'k') { d = 1 } if Input.key_pressed(key: 'k') { p = 1 } print(frame*100 + d*10 + p) } # stdin "xxkkkkxxzz" (2 chars/frame) prints: 100 210 310 400 500 600 # expected frame 2 = 211 (held AND pressed edge), frame 3 = 310 ``` `input_device_commit` snapshots `in_prev <- in_held` before rebuilding `in_held`, and `input_key_pressed` is `held && !prev`, so on paper the edge should appear on the first held frame. It doesn't, so something else is resetting/aliasing `in_prev` (suspects: `in_init()` re-allocating sets, or prev being refreshed again after the rebuild). `key_released` likely shares the fault. Impact: any game verb bound to a press edge (dash, melee, menu confirm) silently never triggers. Workaround: game-side edge detection with a previous-state flag. Windowed path shares the same code, so this is not headless-only.
Author
Owner

Fixed in 0497029 (full suite 110/0, golden renders byte-identical, fixpoint intact).

Root cause — a double-commit, exactly as you suspected (in_prev getting aliased to in_held). Since #83 the generated frame loop commits the device layer once per frame (input_drive). A game that also called Input.poll by hand — which was the only way to get device state before #83 — committed a second time in the same frame. input_device_commit copies in_held into in_prev at the top of every commit, so the second commit made in_prev == in_held, and key_pressed (held && !prev) / key_released could never see a transition. Your repro (which calls Input.poll in the handler) hit exactly this.

Fix — an in_have_frame_driver flag: the loop's input_drive sets it, and a manual Input.poll under the loop becomes a no-op that returns the frame's key instead of re-committing. So there is exactly one commit per frame and the edges fire on the transition frame. An entry-driven harness (no loop) leaves the flag false, so each Input.poll still commits a frame of input as before — the #7/#50 record/replay + device tests are unchanged.

Verified with examples/library/input_edge.ludic — a handler game reading the key edges in Update, driven by stdin xkkxq, prints 0 11 1 100 0: the press edge fires on the frame the key goes down (11), stays held with no edge (1), and the release edge fires on the frame it goes up (100). Your original repro now prints the expected ... 211 310 ... too (frame 2 shows the held+pressed edge). Wired as a feat_case.

Fixed in 0497029 (full suite 110/0, golden renders byte-identical, fixpoint intact). **Root cause** — a double-commit, exactly as you suspected (`in_prev` getting aliased to `in_held`). Since #83 the generated frame loop commits the device layer once per frame (`input_drive`). A game that *also* called `Input.poll` by hand — which was the only way to get device state before #83 — committed a **second** time in the same frame. `input_device_commit` copies `in_held` into `in_prev` at the top of every commit, so the second commit made `in_prev == in_held`, and `key_pressed` (`held && !prev`) / `key_released` could never see a transition. Your repro (which calls `Input.poll` in the handler) hit exactly this. **Fix** — an `in_have_frame_driver` flag: the loop's `input_drive` sets it, and a manual `Input.poll` under the loop becomes a no-op that returns the frame's key instead of re-committing. So there is exactly one commit per frame and the edges fire on the transition frame. An entry-driven harness (no loop) leaves the flag false, so each `Input.poll` still commits a frame of input as before — the #7/#50 record/replay + device tests are unchanged. **Verified** with `examples/library/input_edge.ludic` — a handler game reading the key edges in Update, driven by stdin `xkkxq`, prints `0 11 1 100 0`: the press edge fires on the frame the key goes down (11), stays held with no edge (1), and the release edge fires on the frame it goes up (100). Your original repro now prints the expected `... 211 310 ...` too (frame 2 shows the held+pressed edge). Wired as a feat_case.
orkun closed this issue 2026-09-02 06:27:40 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#87
No description provided.