diff --git a/changes/mouse-delta-rebase.md b/changes/mouse-delta-rebase.md new file mode 100644 index 00000000..62c6023c --- /dev/null +++ b/changes/mouse-delta-rebase.md @@ -0,0 +1,8 @@ +bump: patch +type: fix +**`Input.mouse_dx` / `mouse_dy` no longer jump on the first frame or when the cursor mode changes.** +The delta was the position minus the previous frame's, and the previous position started at 0,0, +so the first frame reported the cursor's whole distance from the corner as motion, and switching +between a locked cursor's virtual reticle and the real cursor did the same. A camera that adds +`mouse_dy` to its pitch came up pointing at the ground with nobody touching the mouse. Both frames +now report no motion; every other frame is unchanged, windowed or headless, on macOS and Windows. diff --git a/examples/library/input_mouse_rebase.ludic b/examples/library/input_mouse_rebase.ludic new file mode 100644 index 00000000..86914b2e --- /dev/null +++ b/examples/library/input_mouse_rebase.ludic @@ -0,0 +1,44 @@ +# input_mouse_rebase.ludic — Input.mouse_dx/dy report no motion on the first frame and on the +# frame a cursor-mode change lands, and ordinary motion on every other frame. +# +# The delta is this frame's position minus the last. Before any position has been read the "last" +# one is 0,0, and across a mode change it belongs to another source (a locked cursor's virtual +# reticle against the real cursor) - both used to come out as a jump the size of the screen. +# Driven headless by injection: +# bin/ludic examples/library/input_mouse_rebase.ludic -> 0 0 5 -2 0 0 4 3 6 0 0 0 +program InputMouseRebase { + entry { + # the first frame: the cursor is already at 300,400, which is not 300,400 of motion + Input.set_mouse(300, 400, 0, 0) + Input.poll() + print(Input.mouse_dx()) # 0 + print(Input.mouse_dy()) # 0 + # ordinary motion + Input.set_mouse(305, 398, 0, 0) + Input.poll() + print(Input.mouse_dx()) # 5 + print(Input.mouse_dy()) # -2 + # a mode change: the next position is from another source + Input.cursor_mode(2) + Input.set_mouse(900, 100, 0, 0) + Input.poll() + print(Input.mouse_dx()) # 0 + print(Input.mouse_dy()) # 0 + Input.set_mouse(904, 103, 0, 0) + Input.poll() + print(Input.mouse_dx()) # 4 + print(Input.mouse_dy()) # 3 + # asking for the mode already in force is not a change + Input.cursor_mode(2) + Input.set_mouse(910, 103, 0, 0) + Input.poll() + print(Input.mouse_dx()) # 6 + print(Input.mouse_dy()) # 0 + # and back: no jump from the reticle to the real cursor + Input.cursor_mode(0) + Input.set_mouse(10, 10, 0, 0) + Input.poll() + print(Input.mouse_dx()) # 0 + print(Input.mouse_dy()) # 0 + } +} diff --git a/runtime/native/input.ludic b/runtime/native/input.ludic index daef3679..843bd947 100644 --- a/runtime/native/input.ludic +++ b/runtime/native/input.ludic @@ -226,6 +226,10 @@ var in_mdy: int = 0 var in_rdx: int = 0 # the raw motion the platform reports while captured var in_rdy: int = 0 var in_cursor_mode: int = 0 +# the next commit reports no mouse delta: true before the first position is read (the previous one +# is not a position, it is 0,0) and after a cursor-mode change (the position source switches between +# the virtual reticle and the real cursor, which are unrelated points) +var in_mouse_rebase: bool = true var in_mbtn: int = 0 # button bitmask (bit 0 left, 1 right, 2 middle) var in_wheel: int = 0 # wheel delta this frame # gamepads: connected flag, button bitmask, and IN_AXES fixed axes each @@ -341,6 +345,10 @@ function input_device_commit(k: int, replaying: int) -> void { in_mdy = in_my - in_my0 # captured (mode 2): the cursor is a clamped reticle, the motion is the raw delta if is_windowed() and in_cursor_mode == 2 { in_mdx = in_rdx; in_mdy = in_rdy } + # No motion on the first frame or across a cursor-mode change. The previous position there is + # 0,0 or a point in another coordinate source, so the difference is the cursor's whole distance + # from it: a camera that adds mouse_dy to its pitch came up pointing at the ground. + if in_mouse_rebase { in_mdx = 0; in_mdy = 0; in_mouse_rebase = false } in_mx0 = in_mx in_my0 = in_my } @@ -445,6 +453,7 @@ enum CursorMode { Normal, Hidden, Locked, Confined } # Input.cursor_mode(mode: enum PadButton { A, B, X, Y, LeftShoulder, RightShoulder, Back, Start } # Input.bind_pad(button:) / pad_button enum MouseButton { Left, Right, Middle } # Input.mouse_down(button:) function input_cursor_mode(mode: int) -> void { + if mode != in_cursor_mode { in_mouse_rebase = true } in_cursor_mode = mode if is_windowed() { win_cursor_mode(mode) } } diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index bef06dd7..87acc6a2 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -675,6 +675,7 @@ function cmd_dev_test() -> int { # deterministic input record/replay — fed one key per poll from stdin. feat_case("library/input_actions", " xwa", "1 0 1 1 0 1 0", "input_actions.ludic (#7 action maps + rebinding + deterministic 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_mouse_rebase", "", "0 0 5 -2 0 0 4 3 6 0 0 0", "input_mouse_rebase.ludic (mouse_dx/dy report no motion on the first frame or across a cursor-mode change)") 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_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)")