fix(input): #87 key_pressed/key_released edges never fired under the frame loop
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m21s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s

Since #83 the loop commits the device layer once per frame (input_drive), but a
game that ALSO called Input.poll by hand 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 left in_prev == in_held and the edges (held &&
!prev) could never see a transition.

Fix: an in_have_frame_driver flag. The loop's input_drive sets it; a manual
Input.poll under the loop then becomes a no-op returning the frame's key
instead of re-committing. An entry-driven harness has no loop, so the flag
stays false and each Input.poll commits a frame as before (the #7/#50
record/replay + device tests are unchanged). Frame loop now calls input_drive.

Example input_edge (press edge on the down frame, release edge on the up
frame). Full suite 110/0, goldens byte-identical, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 07:27:24 +03:00
parent bdf1a97550
commit 0497029dae
6 changed files with 64 additions and 12 deletions

View file

@ -142,14 +142,19 @@ function input_slot_has(s: int, k: int) -> bool {
return false
}
# Advance one frame of input and return the frame's key. This is the single
# per-frame input read: call it once at the top of a frame.
# live — read the live key (rt_poll).
# record — read the live key and append it to the recording.
# replay — take the next key from the recording (the live device is ignored).
# It also advances the multi-key device layer below (held keys, analog, mouse),
# snapshotting or replaying the full per-frame state — #50 extends #7's tape.
function input_poll() -> int {
# #87 — a frame-loop game now has its device layer committed automatically by the
# generated loop (which calls input_drive once per frame). in_have_frame_driver
# records that a loop is driving input, so a *manual* Input.poll in a handler
# becomes a no-op instead of committing a second time in the same frame — a double
# commit copied in_held into in_prev twice, which destroyed the key_pressed /
# key_released edges (in_prev ended up equal to in_held). An entry-driven harness
# has no loop, so the flag stays false and each Input.poll commits a frame as before.
var in_have_frame_driver: bool = false
# The actual per-frame input read: read the live key (or a recorded one), advance
# the record/replay tape, and rebuild the multi-key device layer (held keys, mouse,
# gamepad — #50). Returns the frame's key.
function input_commit() -> int {
input_last = input_frame
if input_mode == 2 { # replay
var k = 0
@ -168,6 +173,22 @@ function input_poll() -> int {
return k
}
# Called by the generated frame loop once per frame (#83). Marks that a loop is
# driving input so a later manual Input.poll this frame does not double-commit.
function input_drive() -> int {
in_have_frame_driver = true
return input_commit()
}
# Input.poll — the single per-frame input read a game can call by hand. In a
# frame-loop game the loop already drove input this frame (input_drive), so this is
# a no-op that returns the frame's key; in an entry-driven harness (no loop) it
# commits a frame of input each call, exactly as before.
function input_poll() -> int {
if in_have_frame_driver { return input_frame }
return input_commit()
}
# ============================================================================
# device layer (#50) — multiple simultaneous keys, analog axes/vectors, the
# mouse, gamepads and touch, plus a full-state record/replay snapshot.