fix(input): #87 key_pressed/key_released edges never fired under the frame loop
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:
parent
bdf1a97550
commit
0497029dae
6 changed files with 64 additions and 12 deletions
3
changes/input-edge-fix.md
Normal file
3
changes/input-edge-fix.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
Input.key_pressed / key_released edges now fire (#87). In a frame-loop game the edges never triggered: the loop (since #83) commits the device layer once per frame via `input_drive`, but a game that *also* called `Input.poll` by hand committed a second time in the same frame, and `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 `key_pressed` (`held && !prev`) / `key_released` could never see a transition. Fixed with 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. An entry-driven harness has no loop, so the flag stays false and each `Input.poll` still commits a frame of input as before (record/replay and the #50 device tests are unchanged). Example: `examples/library/input_edge.ludic` (press edge on the down frame, release edge on the up frame).
|
||||||
27
examples/library/input_edge.ludic
Normal file
27
examples/library/input_edge.ludic
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# input_edge.ludic — Input.key_pressed / key_released edge detection (#87). A
|
||||||
|
# handler game reads the press and release edges of a key in its Update phase; the
|
||||||
|
# frame loop commits the device layer itself (#83), so the game does NOT call
|
||||||
|
# Input.poll by hand, and the edges fire on exactly the transition frame. (The bug:
|
||||||
|
# a manual Input.poll under the loop used to double-commit and copy in_held into
|
||||||
|
# in_prev twice, so key_pressed/key_released never fired.)
|
||||||
|
#
|
||||||
|
# Driven headless by the keys on stdin, one char per frame. With "xkkxq":
|
||||||
|
# x frame0 — nothing
|
||||||
|
# k frame1 — pressed edge (down + just went down)
|
||||||
|
# k frame2 — held (down, no edge)
|
||||||
|
# x frame3 — released edge (was k, now not)
|
||||||
|
# q frame4 — nothing, then quit
|
||||||
|
#
|
||||||
|
# Run: printf 'xkkxq' | bin/ludic examples/library/input_edge.ludic -> 0 11 1 100 0
|
||||||
|
program InputEdge {
|
||||||
|
property Tag { n: int = 0 }
|
||||||
|
model P { Tag }
|
||||||
|
|
||||||
|
handler In phase Update {
|
||||||
|
var v = 0
|
||||||
|
if Input.key_down('k') { v = v + 1 } # held
|
||||||
|
if Input.key_pressed('k') { v = v + 10 } # went down this frame
|
||||||
|
if Input.key_released('k') { v = v + 100 } # went up this frame
|
||||||
|
print(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -142,14 +142,19 @@ function input_slot_has(s: int, k: int) -> bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
# Advance one frame of input and return the frame's key. This is the single
|
# #87 — a frame-loop game now has its device layer committed automatically by the
|
||||||
# per-frame input read: call it once at the top of a frame.
|
# generated loop (which calls input_drive once per frame). in_have_frame_driver
|
||||||
# live — read the live key (rt_poll).
|
# records that a loop is driving input, so a *manual* Input.poll in a handler
|
||||||
# record — read the live key and append it to the recording.
|
# becomes a no-op instead of committing a second time in the same frame — a double
|
||||||
# replay — take the next key from the recording (the live device is ignored).
|
# commit copied in_held into in_prev twice, which destroyed the key_pressed /
|
||||||
# It also advances the multi-key device layer below (held keys, analog, mouse),
|
# key_released edges (in_prev ended up equal to in_held). An entry-driven harness
|
||||||
# snapshotting or replaying the full per-frame state — #50 extends #7's tape.
|
# has no loop, so the flag stays false and each Input.poll commits a frame as before.
|
||||||
function input_poll() -> int {
|
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
|
input_last = input_frame
|
||||||
if input_mode == 2 { # replay
|
if input_mode == 2 { # replay
|
||||||
var k = 0
|
var k = 0
|
||||||
|
|
@ -168,6 +173,22 @@ function input_poll() -> int {
|
||||||
return k
|
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
|
# device layer (#50) — multiple simultaneous keys, analog axes/vectors, the
|
||||||
# mouse, gamepads and touch, plus a full-state record/replay snapshot.
|
# mouse, gamepads and touch, plus a full-state record/replay snapshot.
|
||||||
|
|
|
||||||
|
|
@ -473,8 +473,8 @@ function emit_game_main() -> void {
|
||||||
# mouse and gamepad state). Its return is the frame key, so Input.key still works
|
# mouse and gamepad state). Its return is the frame key, so Input.key still works
|
||||||
# and the game no longer has to call Input.poll by hand. A game that uses no Input
|
# and the game no longer has to call Input.poll by hand. A game that uses no Input
|
||||||
# runtime keeps the plain rt_poll path, byte-identical.
|
# runtime keeps the plain rt_poll path, byte-identical.
|
||||||
if g_uses_input and (find_fn("input_poll") != null) {
|
if g_uses_input and (find_fn("input_drive") != null) {
|
||||||
let k = emit_bind("call i32 @fn_input_poll()")
|
let k = emit_bind("call i32 @fn_input_drive()")
|
||||||
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
|
emit(" store i32 "); emit(k); emit(", ptr @L_key\n")
|
||||||
} else {
|
} else {
|
||||||
if (find_fn("rt_poll") != null) {
|
if (find_fn("rt_poll") != null) {
|
||||||
|
|
|
||||||
|
|
@ -9471,8 +9471,8 @@ declare void @hs_free(i32)
|
||||||
@.str9299 = private unnamed_addr constant [9 x i8] c" br i1 \00"
|
@.str9299 = private unnamed_addr constant [9 x i8] c" br i1 \00"
|
||||||
@.str9300 = private unnamed_addr constant [28 x i8] c", label %body, label %done\0A\00"
|
@.str9300 = private unnamed_addr constant [28 x i8] c", label %body, label %done\0A\00"
|
||||||
@.str9301 = private unnamed_addr constant [7 x i8] c"body:\0A\00"
|
@.str9301 = private unnamed_addr constant [7 x i8] c"body:\0A\00"
|
||||||
@.str9302 = private unnamed_addr constant [11 x i8] c"input_poll\00"
|
@.str9302 = private unnamed_addr constant [12 x i8] c"input_drive\00"
|
||||||
@.str9303 = private unnamed_addr constant [26 x i8] c"call i32 @fn_input_poll()\00"
|
@.str9303 = private unnamed_addr constant [27 x i8] c"call i32 @fn_input_drive()\00"
|
||||||
@.str9304 = private unnamed_addr constant [13 x i8] c" store i32 \00"
|
@.str9304 = private unnamed_addr constant [13 x i8] c" store i32 \00"
|
||||||
@.str9305 = private unnamed_addr constant [14 x i8] c", ptr @L_key\0A\00"
|
@.str9305 = private unnamed_addr constant [14 x i8] c", ptr @L_key\0A\00"
|
||||||
@.str9306 = private unnamed_addr constant [8 x i8] c"rt_poll\00"
|
@.str9306 = private unnamed_addr constant [8 x i8] c"rt_poll\00"
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,7 @@ function cmd_test() -> int {
|
||||||
feat_case("library/input_device", "", "1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1", "input_device.ludic (#50 multi-key held state + analog axis/vector + mouse + gamepad/touch + full-state replay)")
|
feat_case("library/input_device", "", "1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1", "input_device.ludic (#50 multi-key held state + analog axis/vector + mouse + gamepad/touch + full-state replay)")
|
||||||
feat_case("library/input_manager", "", "1 1 1 0 1 0 0 1", "input_manager.ludic (#83 Input Manager: default bindings + device-agnostic actions across keyboard/pad + on-press/on-release edges)")
|
feat_case("library/input_manager", "", "1 1 1 0 1 0 0 1", "input_manager.ludic (#83 Input Manager: default bindings + device-agnostic actions across keyboard/pad + on-press/on-release edges)")
|
||||||
feat_case("library/input_auto", "llq", "2", "input_auto.ludic (#83 the frame loop auto-commits the device layer: Input.active fires with no manual Input.poll)")
|
feat_case("library/input_auto", "llq", "2", "input_auto.ludic (#83 the frame loop auto-commits the device layer: Input.active fires with no manual Input.poll)")
|
||||||
|
feat_case("library/input_edge", "xkkxq", "0 11 1 100 0", "input_edge.ludic (#87 Input.key_pressed/key_released edges fire on the transition frame; no double-commit under the frame loop)")
|
||||||
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
feat_case("library/audio", "", "0 0 0 0 1", "audio.ludic (#22 Audio.* load/play/music/volume/pitch/stop/is_playing — headless no-op)")
|
||||||
# #6 Http.* — the client links Foundation (macOS-only), so build it through the
|
# #6 Http.* — the client links Foundation (macOS-only), so build it through the
|
||||||
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
# canonical `ludicc -o` path (which wires the framework) and gate on Darwin. The
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue