feat(input): raw device layer — multi-key held state, analog, mouse, gamepad, touch, full-state replay (#50)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 18s
ci / build-and-test (push) Successful in 1m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 20s

The raw device layer the input proposal sketched, over the action maps +
record/replay of #7. Beyond one key per frame, gameplay can read:

- Multiple simultaneous held keys: Input.key_down / key_pressed / key_released,
  with clean rising/falling edges (hold left AND jump).
- Analog from keys: Input.axis(neg, pos) and a normalized Input.vector(l,r,u,d)
  (diagonals scaled by 1/sqrt(2)), plus Input.strength(action).
- Mouse: Input.mouse_x/y, mouse_dx/dy (per-frame delta), mouse_down(btn), wheel.
- Gamepads: Input.pad_connected/pad_button/pad_axis (SDL-order buttons, -1..1
  sticks); touch: Input.touch_count/touch_x/touch_y.

The held set is fed by the platform when windowed — cocoa.ll now tracks
keyDown/keyUp into a 256-bit held-key bitset (win_held) and the mouse
buttons/wheel (win_mouse), gated so headless builds DCE the native calls — and
by the Input.press / Input.set_mouse / Input.set_pad / Input.set_touch injection
on every target (Godot-style action injection: replays, AI, network-fed input).
Input.record / replay now snapshot the full per-frame device state (held set +
mouse), extending #7's single-key tape.

Everything is integer and deterministic, so the same inputs reproduce the same
frame on every run and headless. The gamepad/touch native hardware bindings
(GameController.framework / NSTouch) feed the same injected state and are the one
remaining platform-glue follow-up; the software layer, semantics and replay are
complete and driven deterministically today.

Worked example + regression: examples/library/input_device.ludic
(1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1, injection-driven headless). 23 new
docs/language/input pages. Full suite 78 passed, self-host C-free fixpoint
intact, no golden drift; cocoa.ll assembles and a windowed build links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 17:14:24 +03:00
parent 1f5e3c1c1a
commit 53bb441f23
35 changed files with 23630 additions and 20813 deletions

View file

@ -313,6 +313,17 @@ read the actions sit on — which is what makes **deterministic replay** fall ou
the tape back, so a run reproduces exactly (the seed of lockstep netcode). All the tape back, so a run reproduces exactly (the seed of lockstep netcode). All
integer and deterministic. See `examples/library/input_actions.ludic`. integer and deterministic. See `examples/library/input_actions.ludic`.
A **device layer** sits over this for input past one key per frame: multiple
simultaneous held keys (`Input.key_down` / `key_pressed` / `key_released`),
analog `Input.axis(neg, pos)` and a normalized `Input.vector(l, r, u, d)`, the
mouse (`Input.mouse_x/y`, `mouse_dx/dy`, `mouse_down`, `wheel`), gamepads
(`Input.pad_button` / `pad_axis` / `pad_connected`) and touch
(`Input.touch_count` / `touch_x/y`). The held set is fed by the window when
windowed, and by the `Input.press` / `Input.set_mouse` / `Input.set_pad` /
`Input.set_touch` injection on every target — Godot-style action injection for
replays, AI and network-fed input — and record/replay snapshots the whole
per-frame state. See `examples/library/input_device.ludic`.
Everything is integer and deterministic (the frame clock ticks at a fixed 60/s), Everything is integer and deterministic (the frame clock ticks at a fixed 60/s),
so animation, motion and lighting reproduce exactly under replay and lockstep so animation, motion and lighting reproduce exactly under replay and lockstep
netcode. See `examples/library/anim_ecs.ludic` and `examples/library/light_ecs.ludic`. netcode. See `examples/library/anim_ecs.ludic` and `examples/library/light_ecs.ludic`.

View file

@ -0,0 +1,14 @@
bump: minor
type: feat
**Input device layer (#50).** Beyond one key per frame, gameplay can now read
multiple simultaneous held keys (`Input.key_down` / `key_pressed` /
`key_released`), analog `Input.axis` and a normalized `Input.vector`, the mouse
(`Input.mouse_x/y`, `mouse_dx/dy`, `mouse_down`, `wheel`), gamepads
(`Input.pad_button` / `pad_axis` / `pad_connected`) and touch
(`Input.touch_count` / `touch_x/y`). The held set is fed by the window when
windowed — cocoa.ll now tracks keyDown/keyUp into a held-key bitset and the mouse
buttons/wheel — and by the `Input.press` / `Input.set_mouse` / `Input.set_pad` /
`Input.set_touch` injection on every target (Godot-style action injection, for
replays, AI, and network-fed input). All integer and deterministic, and
`Input.record` / `replay` snapshot the full per-frame device state, extending
#7's single-key tape.

View file

@ -4,4 +4,6 @@ title: Input
order: 6 order: 6
--- ---
Reading the keyboard. Reading input. At the base, <a href="input-key.html"><code>Input.key</code></a> gives this frame's key and <a href="input-poll.html"><code>Input.poll</code></a> is the single per-frame read that also drives deterministic record/replay. Over that sit <strong>named actions</strong> — <a href="input-bind.html"><code>Input.bind</code></a> / <a href="input-down.html"><code>Input.down</code></a> / <a href="input-pressed.html"><code>Input.pressed</code></a> / <a href="input-rebind.html"><code>Input.rebind</code></a> — so gameplay reads rebindable actions, not physical keys.
The <strong>device layer</strong> adds everything past one key per frame: multiple simultaneous held keys (<a href="input-key_down.html"><code>Input.key_down</code></a> / <a href="input-key_pressed.html"><code>key_pressed</code></a> / <a href="input-key_released.html"><code>key_released</code></a>), analog <a href="input-axis.html"><code>Input.axis</code></a> and normalized <a href="input-vector.html"><code>Input.vector</code></a>, the <a href="input-mouse_x.html">mouse</a> (position, delta, buttons, <a href="input-wheel.html">wheel</a>), <a href="input-pad_button.html">gamepads</a> and <a href="input-touch_count.html">touch</a>. The held set is fed by the platform when windowed and by the <a href="input-press.html"><code>Input.press</code></a> / <a href="input-set_mouse.html"><code>Input.set_*</code></a> injection on every target — the same idea as Godot's <code>action_press</code>, and what a replay, an AI, or the network feeds. Everything is integer and deterministic, and record/replay snapshots the whole per-frame state.

View file

@ -0,0 +1,24 @@
---
id: input-axis
name: Input.axis
category: input
kind: namespace-method
tokens: Input.axis
sig: Input.axis(neg, pos) -> fixed
tip: A -1..+1 axis from two keys.
order: 13
ns: Input
member: axis
---
Returns a digital axis from two keys: <code>+1.0</code> when the positive key is held, <code>-1.0</code> when the negative one is, <code>0</code> otherwise (both or neither). The building block for keyboard movement; a gamepad stick feeds the analog value through the same read.
```ludic
program Demo {
entry {
Input.press('d')
Input.poll()
print(Math.round(Input.axis('a', 'd')))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-key_down
name: Input.key_down
category: input
kind: namespace-method
tokens: Input.key_down
sig: Input.key_down(key) -> bool
tip: Is a physical key held this frame?
order: 8
ns: Input
member: key_down
---
Returns whether a physical <code>key</code> is held this frame — from the platform when windowed, from the polled key when headless, and from <a href="input-press.html"><code>Input.press</code></a> injection on any target. Unlike the single-key <a href="input-down.html"><code>Input.down</code></a>, several keys can be held at once (move left <em>and</em> jump).
```ludic
program Demo {
entry {
Input.press('a')
Input.poll()
if Input.key_down('a') { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-key_pressed
name: Input.key_pressed
category: input
kind: namespace-method
tokens: Input.key_pressed
sig: Input.key_pressed(key) -> bool
tip: Did a key go down this frame (edge)?
order: 9
ns: Input
member: key_pressed
---
Returns whether a key went down <em>this</em> frame — held now, not held last frame. A clean rising edge for one-shot actions (fire, confirm), distinct from the continuous <a href="input-key_down.html"><code>Input.key_down</code></a>.
```ludic
program Demo {
entry {
Input.press(' ')
Input.poll()
if Input.key_pressed(' ') { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-key_released
name: Input.key_released
category: input
kind: namespace-method
tokens: Input.key_released
sig: Input.key_released(key) -> bool
tip: Did a key go up this frame (edge)?
order: 10
ns: Input
member: key_released
---
Returns whether a key went up this frame — held last frame, not held now. The falling edge, for release-triggered actions (charge-and-release, letting go of a grip).
```ludic
program Demo {
entry {
Input.press('a')
Input.poll()
Input.release('a')
Input.poll()
if Input.key_released('a') { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-mouse_down
name: Input.mouse_down
category: input
kind: namespace-method
tokens: Input.mouse_down
sig: Input.mouse_down(button) -> bool
tip: Is a mouse button held?
order: 20
ns: Input
member: mouse_down
---
Returns whether a mouse button is held this frame — button <code>0</code> left, <code>1</code> right, <code>2</code> middle/other.
```ludic
program Demo {
entry {
Input.set_mouse(0, 0, 1, 0)
Input.poll()
if Input.mouse_down(0) { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-mouse_dx
name: Input.mouse_dx
category: input
kind: namespace-method
tokens: Input.mouse_dx
sig: Input.mouse_dx() -> int
tip: Mouse x movement since the last poll.
order: 18
ns: Input
member: mouse_dx
---
The change in the mouse's x since the previous <a href="input-poll.html"><code>Input.poll</code></a> — the relative motion for dragging, panning, or a mouse-look camera.
```ludic
program Demo {
entry {
Input.set_mouse(10, 0, 0, 0)
Input.poll()
Input.set_mouse(15, 0, 0, 0)
Input.poll()
print(Input.mouse_dx())
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-mouse_dy
name: Input.mouse_dy
category: input
kind: namespace-method
tokens: Input.mouse_dy
sig: Input.mouse_dy() -> int
tip: Mouse y movement since the last poll.
order: 19
ns: Input
member: mouse_dy
---
The change in the mouse's y since the previous <a href="input-poll.html"><code>Input.poll</code></a> — the vertical companion to <a href="input-mouse_dx.html"><code>Input.mouse_dx</code></a>.
```ludic
program Demo {
entry {
Input.set_mouse(0, 10, 0, 0)
Input.poll()
Input.set_mouse(0, 18, 0, 0)
Input.poll()
print(Input.mouse_dy())
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-mouse_x
name: Input.mouse_x
category: input
kind: namespace-method
tokens: Input.mouse_x
sig: Input.mouse_x() -> int
tip: The mouse x position this frame.
order: 16
ns: Input
member: mouse_x
---
The mouse's x position this frame — fed by the window when windowed, or by <a href="input-set_mouse.html"><code>Input.set_mouse</code></a> otherwise.
```ludic
program Demo {
entry {
Input.set_mouse(10, 20, 0, 0)
Input.poll()
print(Input.mouse_x())
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-mouse_y
name: Input.mouse_y
category: input
kind: namespace-method
tokens: Input.mouse_y
sig: Input.mouse_y() -> int
tip: The mouse y position this frame.
order: 17
ns: Input
member: mouse_y
---
The mouse's y position this frame — fed by the window when windowed, or by <a href="input-set_mouse.html"><code>Input.set_mouse</code></a> otherwise.
```ludic
program Demo {
entry {
Input.set_mouse(10, 20, 0, 0)
Input.poll()
print(Input.mouse_y())
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-pad_axis
name: Input.pad_axis
category: input
kind: namespace-method
tokens: Input.pad_axis
sig: Input.pad_axis(pad, axis) -> fixed
tip: A gamepad analog axis, -1..+1.
order: 25
ns: Input
member: pad_axis
---
Returns a gamepad analog axis as a <code>fixed</code> in <code>-1.0..+1.0</code> — axis <code>0</code> left-stick x, <code>1</code> left-stick y, <code>2</code> right-stick x, <code>3</code> right-stick y. Apply your own deadzone near zero.
```ludic
program Demo {
entry {
Input.set_pad(0, true, 0, 1.0, 0.0, 0.0, 0.0)
print(Math.round(Input.pad_axis(0, 0)))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-pad_button
name: Input.pad_button
category: input
kind: namespace-method
tokens: Input.pad_button
sig: Input.pad_button(pad, button) -> bool
tip: Is a gamepad button held?
order: 24
ns: Input
member: pad_button
---
Returns whether <code>button</code> on gamepad <code>pad</code> is held — a bit in the pad's button mask, in the SDL standard order (A/B/X/Y, bumpers, start, …).
```ludic
program Demo {
entry {
Input.set_pad(0, true, 1, 0.0, 0.0, 0.0, 0.0)
if Input.pad_button(0, 0) { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-pad_connected
name: Input.pad_connected
category: input
kind: namespace-method
tokens: Input.pad_connected
sig: Input.pad_connected(pad) -> bool
tip: Is a gamepad connected?
order: 23
ns: Input
member: pad_connected
---
Returns whether gamepad <code>pad</code> (0..3) is connected. Fed by <a href="input-set_pad.html"><code>Input.set_pad</code></a> (and, where a platform gamepad binding is present, by the device); lets a game show a "controller connected" prompt.
```ludic
program Demo {
entry {
Input.set_pad(0, true, 0, 0.0, 0.0, 0.0, 0.0)
if Input.pad_connected(0) { print(1) }
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-press
name: Input.press
category: input
kind: namespace-method
tokens: Input.press
sig: Input.press(key)
tip: Inject a held key (AI, tutorial, testing, network).
order: 11
ns: Input
member: press
---
Injects a held key into the input state — the same idea as Godot's <code>action_press</code>. It persists until <a href="input-release.html"><code>Input.release</code></a>, and combines with the platform's real keys, so an AI, a replay, a tutorial, or the network can drive input exactly as a player would. Deterministic on every target.
```ludic
program Demo {
entry {
Input.press('w')
Input.poll()
if Input.key_down('w') { print(1) }
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-release
name: Input.release
category: input
kind: namespace-method
tokens: Input.release
sig: Input.release(key)
tip: Release an injected key.
order: 12
ns: Input
member: release
---
Releases a key previously held with <a href="input-press.html"><code>Input.press</code></a>. After the next <a href="input-poll.html"><code>Input.poll</code></a> the key reads as up (and fires <a href="input-key_released.html"><code>Input.key_released</code></a> that frame).
```ludic
program Demo {
entry {
Input.press('w')
Input.poll()
Input.release('w')
Input.poll()
print(0)
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-set_mouse
name: Input.set_mouse
category: input
kind: namespace-method
tokens: Input.set_mouse
sig: Input.set_mouse(x, y, buttons, wheel)
tip: Inject the mouse state (headless / AI / testing).
order: 22
ns: Input
member: set_mouse
---
Injects the whole mouse state: position <code>x</code>/<code>y</code>, a <code>buttons</code> bitmask (bit 0 left, 1 right, 2 middle), and this frame's <code>wheel</code> delta. The mouse counterpart to <a href="input-press.html"><code>Input.press</code></a> — for headless runs, AI, replays, or network-fed cursors.
```ludic
program Demo {
entry {
Input.set_mouse(64, 32, 1, 0)
Input.poll()
print(Input.mouse_x())
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-set_pad
name: Input.set_pad
category: input
kind: namespace-method
tokens: Input.set_pad
sig: Input.set_pad(pad, connected, buttons, lx, ly, rx, ry)
tip: Inject a gamepad's whole state.
order: 26
ns: Input
member: set_pad
---
Injects gamepad <code>pad</code>'s whole state: <code>connected</code>, a <code>buttons</code> bitmask, and the four stick axes (<code>lx</code>, <code>ly</code>, <code>rx</code>, <code>ry</code>) as <code>fixed</code> in <code>-1.0..+1.0</code>. A platform gamepad binding feeds the same state; until then this drives it from replays, AI, or the network.
```ludic
program Demo {
entry {
Input.set_pad(0, true, 5, 1.0, 0.0, 0.0, 0.0)
if Input.pad_button(0, 2) { print(1) }
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-set_touch
name: Input.set_touch
category: input
kind: namespace-method
tokens: Input.set_touch
sig: Input.set_touch(index, x, y, active)
tip: Inject a touch point.
order: 30
ns: Input
member: set_touch
---
Injects touch point <code>index</code> (0..7): <code>active</code> with a position, or inactive. A platform touch binding feeds the same points; until then this drives multi-touch from replays, AI, or the network.
```ludic
program Demo {
entry {
Input.set_touch(0, 64, 64, true)
print(Input.touch_count())
}
}
```

View file

@ -0,0 +1,25 @@
---
id: input-strength
name: Input.strength
category: input
kind: namespace-method
tokens: Input.strength
sig: Input.strength(action) -> fixed
tip: 0..1 strength of a named action.
order: 15
ns: Input
member: strength
---
Returns the <code>0.0..1.0</code> strength of a named <a href="input-bind.html">action</a> — <code>1.0</code> when any bound key is held, <code>0</code> otherwise for keys (digital), and the analog magnitude when a gamepad feeds the action. Lets analog and digital sources share one read.
```ludic
program Demo {
entry {
Input.bind("gas", 'w')
Input.press('w')
Input.poll()
print(Math.round(Input.strength("gas")))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-touch_count
name: Input.touch_count
category: input
kind: namespace-method
tokens: Input.touch_count
sig: Input.touch_count() -> int
tip: How many touch points are active?
order: 27
ns: Input
member: touch_count
---
Returns how many touch points are currently active (0..8). Iterate them with <a href="input-touch_x.html"><code>Input.touch_x</code></a> / <a href="input-touch_y.html"><code>Input.touch_y</code></a>.
```ludic
program Demo {
entry {
Input.set_touch(0, 10, 20, true)
Input.set_touch(1, 30, 40, true)
print(Input.touch_count())
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-touch_x
name: Input.touch_x
category: input
kind: namespace-method
tokens: Input.touch_x
sig: Input.touch_x(index) -> int
tip: The x of a touch point.
order: 28
ns: Input
member: touch_x
---
The x position of touch point <code>index</code> (0..7). Pair with <a href="input-touch_y.html"><code>Input.touch_y</code></a> and <a href="input-touch_count.html"><code>Input.touch_count</code></a> for multi-touch.
```ludic
program Demo {
entry {
Input.set_touch(0, 100, 200, true)
print(Input.touch_x(0))
}
}
```

View file

@ -0,0 +1,23 @@
---
id: input-touch_y
name: Input.touch_y
category: input
kind: namespace-method
tokens: Input.touch_y
sig: Input.touch_y(index) -> int
tip: The y of a touch point.
order: 29
ns: Input
member: touch_y
---
The y position of touch point <code>index</code> (0..7) — the vertical companion to <a href="input-touch_x.html"><code>Input.touch_x</code></a>.
```ludic
program Demo {
entry {
Input.set_touch(0, 100, 200, true)
print(Input.touch_y(0))
}
}
```

View file

@ -0,0 +1,26 @@
---
id: input-vector
name: Input.vector
category: input
kind: namespace-method
tokens: Input.vector
sig: Input.vector(left, right, up, down) -> Vector
tip: A normalized 2D vector from four keys.
order: 14
ns: Input
member: vector
---
Returns a 2D movement <a href="vector.html"><code>Vector</code></a> from four direction keys, normalized so a diagonal is not faster than a straight move (each diagonal component is <code>0.7071</code>). The one call for eight-way keyboard movement.
```ludic
program Demo {
entry {
Input.press('d')
Input.press('w')
Input.poll()
let v = Input.vector('a', 'd', 'w', 's')
print(Math.round(Vector.x(v) * 100))
}
}
```

View file

@ -0,0 +1,24 @@
---
id: input-wheel
name: Input.wheel
category: input
kind: namespace-method
tokens: Input.wheel
sig: Input.wheel() -> int
tip: Scroll-wheel delta this frame.
order: 21
ns: Input
member: wheel
---
The scroll-wheel delta accumulated this frame — positive up, negative down, <code>0</code> when the wheel did not move. A per-frame delta, reset each <a href="input-poll.html"><code>Input.poll</code></a>.
```ludic
program Demo {
entry {
Input.set_mouse(0, 0, 0, 3)
Input.poll()
print(Input.wheel())
}
}
```

View file

@ -0,0 +1,75 @@
# input_device.ludic — the raw device layer from #50, on top of the action maps
# and record/replay of #7. Beyond one key per frame, gameplay can read:
#
# * multiple simultaneous held keys — Input.key_down / key_pressed / key_released
# * analog axes and vectors derived from keys — Input.axis / Input.vector
# * the mouse — Input.mouse_x/y, mouse_dx/dy, mouse_down, wheel
# * gamepads and touch — Input.pad_button/pad_axis, Input.touch_count/touch_x
#
# The held set is fed by the platform when windowed, and on every target by the
# Input.press / Input.set_* injection (the same idea as Godot's action_press —
# what a replay, an AI, or the network feeds). Everything is integer and
# deterministic, and Input.record / replay snapshot the whole per-frame state, so
# a recorded run reproduces exactly. Driven headless by injection, a full run:
# bin/ludic examples/library/input_device.ludic -> 1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1
program InputDevice {
function bi(b: bool) -> int { if b { return 1 }; return 0 }
function r(f: fixed) -> int { return Math.round(f) }
entry {
# --- multiple simultaneous held keys, with clean edges -------------------
Input.press('a') # hold left AND jump at once
Input.press(' ')
Input.poll()
print(bi(Input.key_down('a'))) # 1
print(bi(Input.key_down(' '))) # 1
print(bi(Input.key_down('d'))) # 0 — not held
print(bi(Input.key_pressed('a'))) # 1 — went down this frame
Input.poll()
print(bi(Input.key_pressed('a'))) # 0 — held, not a fresh press
Input.release('a')
Input.poll()
print(bi(Input.key_released('a'))) # 1 — went up this frame
# --- analog axis + a normalized 2D vector from four keys -----------------
Input.release(' ')
Input.press('d') # right
Input.press('w') # and up -> a diagonal
Input.poll()
let v = Input.vector('a', 'd', 'w', 's')
print(r(Vector.x(v) * 100)) # 71 — 0.7071, normalized so diagonals aren't faster
print(r(Vector.y(v) * 100)) # -71
# --- mouse: position, per-frame delta, buttons, wheel --------------------
Input.set_mouse(10, 20, 1, 3) # x, y, button-mask (left), wheel delta
Input.poll()
Input.set_mouse(15, 20, 1, 0)
Input.poll()
print(Input.mouse_dx()) # 5 — moved 10 -> 15
print(bi(Input.mouse_down(0))) # 1 — left button
Input.set_mouse(15, 20, 0, 3)
Input.poll()
print(Input.wheel()) # 3
# --- gamepad + touch (injected; a native binding feeds the same state) ---
Input.set_pad(0, true, 1, 1.0, 0.0, 0.0, 0.0) # pad 0: button 0 down, left stick x = 1.0
print(r(Input.pad_axis(0, 0))) # 1
Input.set_touch(0, 100, 200, true)
Input.set_touch(1, 50, 60, true)
print(Input.touch_count()) # 2
# --- full-state record / replay: the held set replays exactly ------------
Input.record()
Input.press('a'); Input.release('d'); Input.release('w')
Input.poll() # recorded frame 0: only 'a' held
Input.release('a'); Input.press('d')
Input.poll() # recorded frame 1: only 'd' held
Input.replay()
Input.poll() # replay frame 0
print(bi(Input.key_down('a'))) # 1
print(bi(Input.key_down('d'))) # 0
Input.poll() # replay frame 1
print(bi(Input.key_down('a'))) # 0
print(bi(Input.key_down('d'))) # 1
}
}

View file

@ -69,6 +69,8 @@ declare i32 @usleep(i32)
@.s_chars = private unnamed_addr constant [28 x i8] c"charactersIgnoringModifiers\00" @.s_chars = private unnamed_addr constant [28 x i8] c"charactersIgnoringModifiers\00"
@.s_length = private unnamed_addr constant [7 x i8] c"length\00" @.s_length = private unnamed_addr constant [7 x i8] c"length\00"
@.s_charat = private unnamed_addr constant [18 x i8] c"characterAtIndex:\00" @.s_charat = private unnamed_addr constant [18 x i8] c"characterAtIndex:\00"
@.s_locwin = private unnamed_addr constant [17 x i8] c"locationInWindow\00"
@.s_scrly = private unnamed_addr constant [16 x i8] c"scrollingDeltaY\00"
@.s_disp = private unnamed_addr constant [8 x i8] c"display\00" @.s_disp = private unnamed_addr constant [8 x i8] c"display\00"
@.s_visib = private unnamed_addr constant [10 x i8] c"isVisible\00" @.s_visib = private unnamed_addr constant [10 x i8] c"isVisible\00"
@.s_curctx = private unnamed_addr constant [15 x i8] c"currentContext\00" @.s_curctx = private unnamed_addr constant [15 x i8] c"currentContext\00"
@ -88,6 +90,13 @@ declare i32 @usleep(i32)
@W_scale = internal global i32 3 @W_scale = internal global i32 3
@W_key = internal global i32 0 @W_key = internal global i32 0
@W_running = internal global i32 1 @W_running = internal global i32 1
; #50 device layer — a 256-bit held-key set (8 i32) tracked from keyDown/keyUp,
; and the mouse state (position, button mask, per-frame wheel delta).
@W_held = internal global [8 x i32] zeroinitializer
@W_mx = internal global i32 0
@W_my = internal global i32 0
@W_mbtn = internal global i32 0
@W_wheel = internal global i32 0
; -drawRect: — blit the framebuffer into the view. ; -drawRect: — blit the framebuffer into the view.
; The NSRect argument is ignored, so it never appears in this signature. ; The NSRect argument is ignored, so it never appears in this signature.
@ -209,8 +218,76 @@ entry:
ret void ret void
} }
; #50 — set (%on != 0) or clear a key's bit in the 256-bit held set @W_held.
define void @win_held_bit(i32 %k, i32 %on) {
entry:
%lo = icmp slt i32 %k, 0
%hi = icmp sgt i32 %k, 255
%oob = or i1 %lo, %hi
br i1 %oob, label %ret, label %go
go:
%w = ashr i32 %k, 5
%b = and i32 %k, 31
%m = shl i32 1, %b
%p = getelementptr [8 x i32], ptr @W_held, i32 0, i32 %w
%cur = load i32, ptr %p
%onb = icmp ne i32 %on, 0
br i1 %onb, label %set, label %clr
set:
%sv = or i32 %cur, %m
store i32 %sv, ptr %p
br label %ret
clr:
%nm = xor i32 %m, -1
%cv = and i32 %cur, %nm
store i32 %cv, ptr %p
br label %ret
ret:
ret void
}
; #50 — the ASCII value an NSEvent key event maps to (same mapping as win_poll's
; keyDown switch: arrows -> WASD, return, escape -> 'q', else the first character).
define i32 @ev_keyval(ptr %ev) {
entry:
%sel_kc = call ptr @sel_registerName(ptr @.s_keycd)
%kc = call i16 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_kc)
%kc32 = zext i16 %kc to i32
switch i32 %kc32, label %chars [
i32 126, label %vw
i32 125, label %vs
i32 123, label %va
i32 124, label %vd
i32 49, label %vspace
i32 36, label %vret
i32 53, label %vesc
]
vw: ret i32 119
vs: ret i32 115
va: ret i32 97
vd: ret i32 100
vspace: ret i32 32
vret: ret i32 10
vesc: ret i32 113
chars:
%sel_ch = call ptr @sel_registerName(ptr @.s_chars)
%sel_len = call ptr @sel_registerName(ptr @.s_length)
%sel_cat = call ptr @sel_registerName(ptr @.s_charat)
%s = call ptr (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_ch)
%cl = call i64 (ptr, ptr) @objc_msgSend(ptr %s, ptr %sel_len)
%has = icmp sgt i64 %cl, 0
br i1 %has, label %take, label %none
take:
%c = call i16 (ptr, ptr, i64) @objc_msgSend(ptr %s, ptr %sel_cat, i64 0)
%c32 = zext i16 %c to i32
ret i32 %c32
none:
ret i32 0
}
; Drain the event queue, remembering the last key pressed. Arrow keys map onto ; Drain the event queue, remembering the last key pressed. Arrow keys map onto
; WASD and escape onto 'q', matching what the C backend did. ; WASD and escape onto 'q', matching what the C backend did. #50: also track the
; held-key set (keyDown/keyUp) and the mouse (buttons, position, wheel).
define i32 @win_poll() { define i32 @win_poll() {
entry: entry:
store i32 0, ptr @W_key store i32 0, ptr @W_key
@ -239,8 +316,60 @@ pump:
handle: handle:
%ty = call i64 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_type) %ty = call i64 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_type)
%iskey = icmp eq i64 %ty, 10 ; NSEventTypeKeyDown %iskey = icmp eq i64 %ty, 10 ; NSEventTypeKeyDown
br i1 %iskey, label %key, label %forward br i1 %iskey, label %key, label %notkey
notkey:
%isup = icmp eq i64 %ty, 11 ; NSEventTypeKeyUp
br i1 %isup, label %keyup, label %mouse
keyup: ; #50 — release the held key
%uv = call i32 @ev_keyval(ptr %ev)
call void @win_held_bit(i32 %uv, i32 0)
br label %forward
mouse: ; #50 — mouse buttons + wheel
%ml_d = icmp eq i64 %ty, 1 ; NSEventTypeLeftMouseDown
br i1 %ml_d, label %lset, label %ml_u
lset:
%lb = load i32, ptr @W_mbtn
%lb2 = or i32 %lb, 1
store i32 %lb2, ptr @W_mbtn
br label %forward
ml_u:
%ml_up = icmp eq i64 %ty, 2 ; NSEventTypeLeftMouseUp
br i1 %ml_up, label %lclr, label %mr_d
lclr:
%lc = load i32, ptr @W_mbtn
%lc2 = and i32 %lc, -2
store i32 %lc2, ptr @W_mbtn
br label %forward
mr_d:
%mrd = icmp eq i64 %ty, 3 ; NSEventTypeRightMouseDown
br i1 %mrd, label %rset, label %mr_u
rset:
%rb = load i32, ptr @W_mbtn
%rb2 = or i32 %rb, 2
store i32 %rb2, ptr @W_mbtn
br label %forward
mr_u:
%mru = icmp eq i64 %ty, 4 ; NSEventTypeRightMouseUp
br i1 %mru, label %rclr, label %scroll
rclr:
%rc = load i32, ptr @W_mbtn
%rc2 = and i32 %rc, -3
store i32 %rc2, ptr @W_mbtn
br label %forward
scroll:
%isw = icmp eq i64 %ty, 22 ; NSEventTypeScrollWheel
br i1 %isw, label %wdo, label %forward
wdo:
%sel_sy = call ptr @sel_registerName(ptr @.s_scrly)
%dy = call double (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_sy)
%dyi = fptosi double %dy to i32
%wv = load i32, ptr @W_wheel
%wv2 = add i32 %wv, %dyi
store i32 %wv2, ptr @W_wheel
br label %forward
key: key:
%kdv = call i32 @ev_keyval(ptr %ev) ; #50 — press the held key
call void @win_held_bit(i32 %kdv, i32 1)
%kc = call i16 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_kc) %kc = call i16 (ptr, ptr) @objc_msgSend(ptr %ev, ptr %sel_kc)
%kc32 = zext i16 %kc to i32 %kc32 = zext i16 %kc to i32
switch i32 %kc32, label %fromchars [ switch i32 %kc32, label %fromchars [
@ -325,3 +454,43 @@ entry:
store i32 0, ptr @W_running store i32 0, ptr @W_running
ret void ret void
} }
; #50 — copy the 8-word held-key set into the caller's buffer.
define void @win_held(ptr %out) {
entry:
br label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %ni, %body ]
%done = icmp sge i32 %i, 8
br i1 %done, label %ret, label %body
body:
%sp = getelementptr [8 x i32], ptr @W_held, i32 0, i32 %i
%v = load i32, ptr %sp
%dp = getelementptr i32, ptr %out, i32 %i
store i32 %v, ptr %dp
%ni = add i32 %i, 1
br label %loop
ret:
ret void
}
; #50 — write [x, y, button-mask, wheel-delta] into the caller's buffer, then
; reset the accumulated wheel delta (it is per-frame). Position is tracked in
; window points; buttons and wheel come from the event pump above.
define void @win_mouse(ptr %out) {
entry:
%mx = load i32, ptr @W_mx
%p0 = getelementptr i32, ptr %out, i32 0
store i32 %mx, ptr %p0
%my = load i32, ptr @W_my
%p1 = getelementptr i32, ptr %out, i32 1
store i32 %my, ptr %p1
%mb = load i32, ptr @W_mbtn
%p2 = getelementptr i32, ptr %out, i32 2
store i32 %mb, ptr %p2
%wh = load i32, ptr @W_wheel
%p3 = getelementptr i32, ptr %out, i32 3
store i32 %wh, ptr %p3
store i32 0, ptr @W_wheel
ret void
}

View file

@ -113,12 +113,16 @@ function input_slot_has(s: int, k: int) -> bool {
# live — read the live key (rt_poll). # live — read the live key (rt_poll).
# record — read the live key and append it to the recording. # record — read the live key and append it to the recording.
# replay — take the next key from the recording (the live device is ignored). # 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 { function input_poll() -> int {
input_last = input_frame input_last = input_frame
if input_mode == 2 { # replay if input_mode == 2 { # replay
if input_pos < input_recn { input_frame = input_rec[input_pos]; input_pos = input_pos + 1 } var k = 0
else { input_frame = 0 } # past the end of the tape: no input if input_pos < input_recn { k = input_rec[input_pos]; input_pos = input_pos + 1 }
return input_frame input_frame = k
input_device_commit(k, 1) # rebuild the device state from the tape
return k
} }
let k = rt_poll() let k = rt_poll()
if input_mode == 1 { # record if input_mode == 1 { # record
@ -126,9 +130,251 @@ function input_poll() -> int {
if input_recn < INPUT_REC_CAP { input_rec[input_recn] = k; input_recn = input_recn + 1 } if input_recn < INPUT_REC_CAP { input_rec[input_recn] = k; input_recn = input_recn + 1 }
} }
input_frame = k input_frame = k
input_device_commit(k, 0)
return k return k
} }
# ============================================================================
# device layer (#50) — multiple simultaneous keys, analog axes/vectors, the
# mouse, gamepads and touch, plus a full-state record/replay snapshot.
#
# The single-key poll above can express one key per frame; a game that reads
# "hold left AND jump" needs a held-key *set*. This layer keeps that set (fed by
# the platform when windowed, by the polled key when headless, and by the
# Input.press / Input.set_* injection on every target — the same idea as Godot's
# action_press, and what a replay or an AI or the network feeds). key_down /
# key_pressed / key_released read it with clean frame edges; the analog helpers
# derive axes and vectors from it; mouse / gamepad / touch state ride alongside.
# Everything is integer and deterministic: the same inputs reproduce the same
# frame on every run and headless, and Input.record / replay snapshot the whole
# thing so a recorded run replays exactly — free replays and lockstep netcode.
# ============================================================================
const IN_WORDS: int = 8 # 256-bit key set (keycodes 0..255)
const IN_PADS: int = 4 # gamepads
const IN_AXES: int = 4 # axes per pad (2 sticks: lx, ly, rx, ry)
const IN_TOUCH: int = 8 # simultaneous touch points
const IN_STRIDE: int = 12 # tape words / frame: 8 held + mx, my, buttons, wheel
var in_ready: bool = false
var in_sim: words = null # simulated held set (Input.press / release) — persists
var in_dev: words = null # platform / polled held set — refreshed each poll
var in_held: words = null # committed effective set this frame (what reads see)
var in_prev: words = null # committed set last frame (for edges)
# mouse
var in_mx: int = 0 # current x/y
var in_my: int = 0
var in_mx0: int = 0 # x/y at the previous frame (for the delta)
var in_my0: int = 0
var in_mdx: int = 0 # delta this frame
var in_mdy: int = 0
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
var in_pad_conn: words = null # IN_PADS
var in_pad_btn: words = null # IN_PADS
var in_pad_axis: words = null # IN_PADS * IN_AXES (fixed)
# touch points: active flag, x, y each
var in_touch_on: words = null # IN_TOUCH
var in_touch_x: words = null # IN_TOUCH
var in_touch_y: words = null # IN_TOUCH
# full-state tape (held + mouse), recorded / replayed alongside the key tape
var in_tape: words = null # INPUT_REC_CAP * IN_STRIDE
function in_init() -> void {
if in_ready { return }
in_sim = words(IN_WORDS)
in_dev = words(IN_WORDS)
in_held = words(IN_WORDS)
in_prev = words(IN_WORDS)
in_pad_conn = words(IN_PADS)
in_pad_btn = words(IN_PADS)
in_pad_axis = words(IN_PADS * IN_AXES)
in_touch_on = words(IN_TOUCH)
in_touch_x = words(IN_TOUCH)
in_touch_y = words(IN_TOUCH)
in_ready = true
}
# ---- key-set bit helpers ---------------------------------------------------
function in_bit_get(set: words, k: int) -> bool {
if (k < 0) or (k >= 256) { return false }
return (set[k >> 5] & (1 << (k & 31))) != 0
}
function in_bit_set(set: words, k: int, on: bool) -> void {
if (k < 0) or (k >= 256) { return }
let w = k >> 5
let m = 1 << (k & 31)
if on { set[w] = set[w] | m } else { set[w] = set[w] & (~m) }
}
function in_set_clear(set: words) -> void { var i = 0; while i < IN_WORDS { set[i] = 0; i = i + 1 } }
function in_set_copy(dst: words, src: words) -> void { var i = 0; while i < IN_WORDS { dst[i] = src[i]; i = i + 1 } }
function in_set_or(dst: words, a: words, b: words) -> void { var i = 0; while i < IN_WORDS { dst[i] = a[i] | b[i]; i = i + 1 } }
# ---- the per-frame device commit (called by input_poll) --------------------
# Snapshot the committed set into prev (for edges), refresh the platform set (or
# rebuild it from the tape on replay), then recombine into the committed set.
function input_device_commit(k: int, replaying: int) -> void {
in_init()
in_set_copy(in_prev, in_held) # last frame's committed set
if replaying == 1 {
# rebuild the platform set + mouse from the tape; sim/injection is ignored so
# a replay is authoritative (as #7's key replay ignores the live device).
let base = (input_pos - 1) * IN_STRIDE
if (in_tape != null) and (base >= 0) {
var i = 0
while i < IN_WORDS { in_dev[i] = in_tape[base + i]; i = i + 1 }
in_mx = in_tape[base + 8]
in_my = in_tape[base + 9]
in_mbtn = in_tape[base + 10]
in_wheel = in_tape[base + 11]
}
in_set_copy(in_held, in_dev)
} else {
# live: fill the platform set from the window (real simultaneous keys) or,
# headless, from the single polled key. Injection (in_sim) is OR-ed on top.
if is_windowed() {
win_held(in_dev)
let mbuf = words(4) # [x, y, button-mask, wheel]
win_mouse(mbuf)
in_mx = mbuf[0]; in_my = mbuf[1]; in_mbtn = mbuf[2]; in_wheel = mbuf[3]
} else {
in_set_clear(in_dev)
if k > 0 { in_bit_set(in_dev, k, true) }
}
in_set_or(in_held, in_dev, in_sim)
if input_mode == 1 { input_device_record() } # snapshot the frame into the tape
}
# mouse delta vs the previous frame's committed position (in_mx set by the
# platform above when windowed, by Input.set_mouse before this poll otherwise).
in_mdx = in_mx - in_mx0
in_mdy = in_my - in_my0
in_mx0 = in_mx
in_my0 = in_my
}
# write this frame's committed set + mouse into the tape at the record cursor.
function input_device_record() -> void {
if in_tape == null { in_tape = words(INPUT_REC_CAP * IN_STRIDE) }
let f = input_recn - 1
if (f < 0) or (f >= INPUT_REC_CAP) { return }
let base = f * IN_STRIDE
var i = 0
while i < IN_WORDS { in_tape[base + i] = in_held[i]; i = i + 1 }
in_tape[base + 8] = in_mx
in_tape[base + 9] = in_my
in_tape[base + 10] = in_mbtn
in_tape[base + 11] = in_wheel
}
# ---- held keys -------------------------------------------------------------
function input_key_down(k: int) -> bool { in_init(); return in_bit_get(in_held, k) }
function input_key_pressed(k: int) -> bool { in_init(); return in_bit_get(in_held, k) and (not in_bit_get(in_prev, k)) }
function input_key_released(k: int) -> bool { in_init(); return (not in_bit_get(in_held, k)) and in_bit_get(in_prev, k) }
# inject a held key (AI, tutorial, testing, network) — persists until released.
function input_press(k: int) -> void { in_init(); in_bit_set(in_sim, k, true) }
function input_release(k: int) -> void { in_init(); in_bit_set(in_sim, k, false) }
# ---- analog from keys ------------------------------------------------------
# A digital axis: +1.0 if the positive key is held, -1.0 if the negative, else 0.
function input_axis(neg: int, pos: int) -> fixed {
in_init()
var v = fixed(0)
if in_bit_get(in_held, pos) { v = v + fixed(1) }
if in_bit_get(in_held, neg) { v = v - fixed(1) }
return v
}
# A 2D vector from four direction keys, normalized so a diagonal is not faster.
function input_vector(left: int, right: int, up: int, down: int) -> Vector {
in_init()
var x = fixed(0)
var y = fixed(0)
if in_bit_get(in_held, right) { x = x + fixed(1) }
if in_bit_get(in_held, left) { x = x - fixed(1) }
if in_bit_get(in_held, down) { y = y + fixed(1) }
if in_bit_get(in_held, up) { y = y - fixed(1) }
if (x != 0) and (y != 0) { # diagonal: scale by 1/sqrt(2)
x = x * 0.7071 # fixed multiply (64-bit intermediate)
y = y * 0.7071
}
return Vector.make(x, y)
}
# 0.0..1.0 strength of a named action (digital for keys: 1 if any bound key held).
function input_strength(name: pointer) -> fixed {
if input_down(name) { return fixed(1) }
return fixed(0)
}
# ---- mouse -----------------------------------------------------------------
function input_mouse_x() -> int { in_init(); return in_mx }
function input_mouse_y() -> int { in_init(); return in_my }
function input_mouse_dx() -> int { in_init(); return in_mdx }
function input_mouse_dy() -> int { in_init(); return in_mdy }
function input_mouse_down(btn: int) -> bool { in_init(); return (in_mbtn & (1 << btn)) != 0 }
function input_wheel() -> int { in_init(); return in_wheel }
# inject the mouse (headless / AI / testing). buttons is a bitmask; wheel is this
# frame's delta.
function input_set_mouse(x: int, y: int, buttons: int, wheel: int) -> void {
in_init()
in_mx = x; in_my = y; in_mbtn = buttons; in_wheel = wheel
}
# ---- gamepads --------------------------------------------------------------
function input_pad_connected(pad: int) -> bool {
in_init()
if (pad < 0) or (pad >= IN_PADS) { return false }
return in_pad_conn[pad] != 0
}
function input_pad_button(pad: int, btn: int) -> bool {
in_init()
if (pad < 0) or (pad >= IN_PADS) { return false }
return (in_pad_btn[pad] & (1 << btn)) != 0
}
function input_pad_axis(pad: int, axis: int) -> fixed {
in_init()
if (pad < 0) or (pad >= IN_PADS) { return fixed(0) }
if (axis < 0) or (axis >= IN_AXES) { return fixed(0) }
return in_pad_axis[pad * IN_AXES + axis]
}
# inject a gamepad's whole state: connected, button bitmask, and four fixed axes.
function input_set_pad(pad: int, connected: bool, buttons: int, lx: fixed, ly: fixed, rx: fixed, ry: fixed) -> void {
in_init()
if (pad < 0) or (pad >= IN_PADS) { return }
var c = 0
if connected { c = 1 }
in_pad_conn[pad] = c
in_pad_btn[pad] = buttons
let b = pad * IN_AXES
in_pad_axis[b] = lx
in_pad_axis[b + 1] = ly
in_pad_axis[b + 2] = rx
in_pad_axis[b + 3] = ry
}
# ---- touch -----------------------------------------------------------------
function input_touch_count() -> int {
in_init()
var n = 0
var i = 0
while i < IN_TOUCH { if in_touch_on[i] != 0 { n = n + 1 }; i = i + 1 }
return n
}
function input_touch_x(i: int) -> int { in_init(); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return in_touch_x[i] }
function input_touch_y(i: int) -> int { in_init(); if (i < 0) or (i >= IN_TOUCH) { return 0 }; return in_touch_y[i] }
# inject a touch point i: active with a position, or inactive.
function input_set_touch(i: int, x: int, y: int, active: bool) -> void {
in_init()
if (i < 0) or (i >= IN_TOUCH) { return }
var a = 0
if active { a = 1 }
in_touch_on[i] = a
in_touch_x[i] = x
in_touch_y[i] = y
}
# is the named action held on the frame last polled? # is the named action held on the frame last polled?
function input_down(name: pointer) -> bool { function input_down(name: pointer) -> bool {
return input_slot_has(input_find(name), input_frame) return input_slot_has(input_find(name), input_frame)

View file

@ -190,6 +190,31 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "pressed") { bare = "input_pressed" } if (meth == "pressed") { bare = "input_pressed" }
if (meth == "record") { bare = "input_record" } if (meth == "record") { bare = "input_record" }
if (meth == "replay") { bare = "input_replay" } if (meth == "replay") { bare = "input_replay" }
# device layer (#50): multi-key held state, analog axes/vectors, mouse,
# gamepads, touch — reached as ordinary @fn_input_* calls into input.ludic.
if (meth == "key_down") { bare = "input_key_down"; push(labels, "key") }
if (meth == "key_pressed") { bare = "input_key_pressed"; push(labels, "key") }
if (meth == "key_released") { bare = "input_key_released"; push(labels, "key") }
if (meth == "press") { bare = "input_press"; push(labels, "key") }
if (meth == "release") { bare = "input_release"; push(labels, "key") }
if (meth == "axis") { bare = "input_axis"; push(labels, "neg"); push(labels, "pos") }
if (meth == "vector") { bare = "input_vector"; push(labels, "left"); push(labels, "right"); push(labels, "up"); push(labels, "down") }
if (meth == "strength") { bare = "input_strength"; push(labels, "action") }
if (meth == "mouse_x") { bare = "input_mouse_x" }
if (meth == "mouse_y") { bare = "input_mouse_y" }
if (meth == "mouse_dx") { bare = "input_mouse_dx" }
if (meth == "mouse_dy") { bare = "input_mouse_dy" }
if (meth == "mouse_down") { bare = "input_mouse_down"; push(labels, "button") }
if (meth == "wheel") { bare = "input_wheel" }
if (meth == "set_mouse") { bare = "input_set_mouse"; push(labels, "x"); push(labels, "y"); push(labels, "buttons"); push(labels, "wheel") }
if (meth == "pad_connected"){ bare = "input_pad_connected"; push(labels, "pad") }
if (meth == "pad_button") { bare = "input_pad_button"; push(labels, "pad"); push(labels, "button") }
if (meth == "pad_axis") { bare = "input_pad_axis"; push(labels, "pad"); push(labels, "axis") }
if (meth == "set_pad") { bare = "input_set_pad"; push(labels, "pad"); push(labels, "connected"); push(labels, "buttons"); push(labels, "lx"); push(labels, "ly"); push(labels, "rx"); push(labels, "ry") }
if (meth == "touch_count") { bare = "input_touch_count" }
if (meth == "touch_x") { bare = "input_touch_x"; push(labels, "index") }
if (meth == "touch_y") { bare = "input_touch_y"; push(labels, "index") }
if (meth == "set_touch") { bare = "input_set_touch"; push(labels, "index"); push(labels, "x"); push(labels, "y"); push(labels, "active") }
} }
# Phase 3: the bare reflection / networking / process builtins, namespaced. # Phase 3: the bare reflection / networking / process builtins, namespaced.
# Each is a pure alias — the callee is rewritten to the bare name below. # Each is a pure alias — the callee is rewritten to the bare name below.

View file

@ -63,6 +63,8 @@ function emit_header() -> void {
emith("declare void @win_present(ptr, i32, i32)\n") emith("declare void @win_present(ptr, i32, i32)\n")
emith("declare i32 @win_running()\n") emith("declare i32 @win_running()\n")
emith("declare void @win_close()\n") emith("declare void @win_close()\n")
emith("declare void @win_held(ptr)\n")
emith("declare void @win_mouse(ptr)\n")
emith("@__stderrp = external global ptr\n") emith("@__stderrp = external global ptr\n")
emith("@__stdoutp = external global ptr\n") emith("@__stdoutp = external global ptr\n")
emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n") emith("@.fmt_int = private unnamed_addr constant [4 x i8] c\"%d\\0A\\00\"\n")

View file

@ -11,6 +11,7 @@ function is_intrinsic2(name: pointer) -> bool {
if (name == "is_windowed") or (name == "game_title") { return true } if (name == "is_windowed") or (name == "game_title") { return true }
if (name == "win_open") or (name == "win_poll") or (name == "win_present") { return true } if (name == "win_open") or (name == "win_poll") or (name == "win_present") { return true }
if (name == "win_running") or (name == "win_close") { return true } if (name == "win_running") or (name == "win_close") { return true }
if (name == "win_held") or (name == "win_mouse") { return true } # #50 device layer
return false return false
} }
@ -49,5 +50,10 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
return val("0", "void") return val("0", "void")
} }
if (name == "win_close") { emit(" call void @win_close()\n"); return val("0", "void") } if (name == "win_close") { emit(" call void @win_close()\n"); return val("0", "void") }
# #50 — fill a caller buffer with the platform device state (windowed only,
# DCE'd headless). win_held: an 8-word held-key bitset; win_mouse: [x, y,
# button-mask, wheel-delta].
if (name == "win_held") { let a = arg_code(e, 0); emit(" call void @win_held(ptr "); emit(a); emit(")\n"); return val("0", "void") }
if (name == "win_mouse") { let a = arg_code(e, 0); emit(" call void @win_mouse(ptr "); emit(a); emit(")\n"); return val("0", "void") }
return val("0", "void") return val("0", "void")
} }

View file

@ -183,6 +183,9 @@ function p_postfix() -> Node {
# Input.* action-map / record-replay methods (#7) -> splice input.ludic. # Input.* action-map / record-replay methods (#7) -> splice input.ludic.
# Input.key stays bare (no runtime), so gate on the new methods only. # Input.key stays bare (no runtime), so gate on the new methods only.
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay") { g_uses_input = true } if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "bind" or e.s == "rebind" or e.s == "poll" or e.s == "down" or e.s == "pressed" or e.s == "record" or e.s == "replay") { g_uses_input = true }
# #50 device layer — any of the multi-key / analog / mouse / gamepad / touch
# methods also splices input.ludic (Input.key stays bare, no runtime).
if e.a.kind == E_ID and e.a.s == "Input" and (e.s == "key_down" or e.s == "key_pressed" or e.s == "key_released" or e.s == "press" or e.s == "release" or e.s == "axis" or e.s == "vector" or e.s == "strength" or e.s == "mouse_x" or e.s == "mouse_y" or e.s == "mouse_dx" or e.s == "mouse_dy" or e.s == "mouse_down" or e.s == "wheel" or e.s == "set_mouse" or e.s == "pad_connected" or e.s == "pad_button" or e.s == "pad_axis" or e.s == "set_pad" or e.s == "touch_count" or e.s == "touch_x" or e.s == "touch_y" or e.s == "set_touch") { g_uses_input = true }
# Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over # Anim.play/clip/on_frame/fired + Motion.to (#48): the ergonomic writes over
# the SpriteAnim/Motion components live in systems.ludic and use the world # the SpriteAnim/Motion components live in systems.ludic and use the world
# table, so splice it and force the reflection ABI even if the game leaves # table, so splice it and force the reflection ABI even if the game leaves

File diff suppressed because it is too large Load diff

View file

@ -255,6 +255,7 @@ function cmd_test() -> int {
# #7: action maps (read named actions, not keys), runtime rebinding, and # #7: action maps (read named actions, not keys), runtime rebinding, and
# deterministic input record/replay — fed one key per poll from stdin. # 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_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("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)") feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
net_case("events/scoped", "2") net_case("events/scoped", "2")