feat(input): #89 cursor capture — Input.cursor_mode (hide/lock/confine)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 22s
ci / build-and-test (push) Successful in 2m24s
commit-lint / conventional-commits (push) Successful in 5s
docs / build-and-deploy (push) Successful in 29s

A windowed action game can hide the OS cursor and lock/confine the mouse to the
window. Input.cursor_mode(mode): 0 normal, 1 hidden (draw your own reticle),
2 locked (hidden + dissociated — the mouse feeds relative motion via
Input.mouse_dx/dy and Input.mouse_x/y is a clamped virtual cursor, FPS/twin-stick
aim), 3 confined (dissociated but visible; the mouse can't leave the window).
The platform auto-releases (shows + reconnects) while the window is not key
(Cmd-Tab) and on close, so the cursor is never left captured. Headless it is a
no-op (DCE'd).

Native macOS impl in cocoa.ll: [NSCursor hide]/[unhide] (ref-counted, toggled
only on change so the count stays balanced across focus changes),
CGAssociateMouseAndMouseCursorPosition, and CGGetLastMouseDelta for the relative
virtual cursor, behind a new win_cursor_mode intrinsic. Windowed-only behaviour
(not in the headless golden suite); example compiles headless and links
windowed. Full suite 115/0, fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-02 08:06:34 +03:00
parent f3f1336882
commit 3eb5447f74
11 changed files with 26543 additions and 26240 deletions

View file

@ -41,6 +41,10 @@ declare void @CGImageRelease(ptr)
declare void @CGContextRelease(ptr)
declare void @CGColorSpaceRelease(ptr)
declare i32 @usleep(i32)
; #89 — cursor capture: NSCursor hide/unhide (ref-counted) + the CoreGraphics
; mouse-association / relative-delta calls.
declare i32 @CGAssociateMouseAndMouseCursorPosition(i32)
declare void @CGGetLastMouseDelta(ptr, ptr)
@.c_app = private unnamed_addr constant [14 x i8] c"NSApplication\00"
@.c_win = private unnamed_addr constant [9 x i8] c"NSWindow\00"
@ -49,6 +53,10 @@ declare i32 @usleep(i32)
@.c_view = private unnamed_addr constant [7 x i8] c"NSView\00"
@.c_gctx = private unnamed_addr constant [18 x i8] c"NSGraphicsContext\00"
@.c_ludic = private unnamed_addr constant [10 x i8] c"LudicView\00"
@.c_cursor = private unnamed_addr constant [9 x i8] c"NSCursor\00"
@.s_hide = private unnamed_addr constant [5 x i8] c"hide\00"
@.s_unhide = private unnamed_addr constant [7 x i8] c"unhide\00"
@.s_iskey = private unnamed_addr constant [12 x i8] c"isKeyWindow\00"
@.s_shared = private unnamed_addr constant [18 x i8] c"sharedApplication\00"
@.s_policy = private unnamed_addr constant [21 x i8] c"setActivationPolicy:\00"
@ -143,6 +151,14 @@ declare i32 @usleep(i32)
; #51 — up to 8 trackpad touch points, [active, x, y] each (framebuffer pixels),
; refreshed by the view's touchesBegan/Moved/Ended handlers.
@W_touch = internal global [24 x i32] zeroinitializer
; #89 — cursor capture. @W_cursor_mode is the desired mode (0 normal, 1 hidden,
; 2 locked+relative, 3 confined). @W_cursor_hidden / @W_cursor_assoc track the
; currently-applied NSCursor hide (ref-counted, so we only toggle on a change) and
; the CGAssociateMouseAndMouseCursorPosition state, so focus loss auto-releases and
; refocus re-acquires without unbalancing the hide counter.
@W_cursor_mode = internal global i32 0
@W_cursor_hidden = internal global i32 0
@W_cursor_assoc = internal global i32 1
; -drawRect: — blit the framebuffer into the view.
; The NSRect argument is ignored, so it never appears in this signature.
@ -477,6 +493,7 @@ dosend:
%fwd = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %app, ptr %sel_send, ptr %ev)
br label %pump
finish:
call void @win_cursor_maintain() ; #89 — keep the cursor state (auto-release on focus loss)
; a closed window ends the run
%win = load ptr, ptr @W_win
%hw = icmp ne ptr %win, null
@ -519,6 +536,9 @@ entry:
define void @win_close() {
entry:
; #89 — restore the cursor so it is never left hidden / dissociated after quit.
store i32 0, ptr @W_cursor_mode
call void @cursor_apply(i32 0, i32 1)
store i32 0, ptr @W_running
ret void
}
@ -550,6 +570,41 @@ entry:
; #51 — read the live cursor position from the window (points, origin
; bottom-left) and convert to framebuffer pixels, y-flipped so row 0 is the
; top, matching the framebuffer the game draws into.
; #89 — in a relative mode (locked / confined) the OS cursor is dissociated, so
; mouseLocation is frozen; accumulate CGGetLastMouseDelta into a virtual cursor
; (clamped to the framebuffer) so Input.mouse_dx/dy report the motion and
; Input.mouse_x/y give a reticle position.
%cmode = load i32, ptr @W_cursor_mode
%isrel = icmp sge i32 %cmode, 2
br i1 %isrel, label %reldelta, label %abspos
reldelta:
%dxp = alloca i32
%dyp = alloca i32
call void @CGGetLastMouseDelta(ptr %dxp, ptr %dyp)
%ddx = load i32, ptr %dxp
%ddy = load i32, ptr %dyp
%omx = load i32, ptr @W_mx
%omy = load i32, ptr @W_my
%nmx = add i32 %omx, %ddx
%nmy = add i32 %omy, %ddy
; clamp x to [0, fbw-1]
%fbwv = load i32, ptr @W_fbw
%fbwm = sub i32 %fbwv, 1
%xlo = icmp slt i32 %nmx, 0
%nmx1 = select i1 %xlo, i32 0, i32 %nmx
%xhi = icmp sgt i32 %nmx1, %fbwm
%nmx2 = select i1 %xhi, i32 %fbwm, i32 %nmx1
; clamp y to [0, fbh-1]
%fbhv = load i32, ptr @W_fbh
%fbhm = sub i32 %fbhv, 1
%ylo = icmp slt i32 %nmy, 0
%nmy1 = select i1 %ylo, i32 0, i32 %nmy
%yhi = icmp sgt i32 %nmy1, %fbhm
%nmy2 = select i1 %yhi, i32 %fbhm, i32 %nmy1
store i32 %nmx2, ptr @W_mx
store i32 %nmy2, ptr @W_my
br label %emit
abspos:
%win = load ptr, ptr @W_win
%nowin = icmp eq ptr %win, null
br i1 %nowin, label %emit, label %qpos
@ -587,6 +642,84 @@ emit:
ret void
}
; ============================================================================
; #89 — cursor capture. Apply the currently-wanted hidden / associated state,
; toggling only on a change so the ref-counted [NSCursor hide]/[unhide] stays
; balanced across frames and focus changes.
define void @cursor_apply(i32 %wh, i32 %wa) {
entry:
%ch = load i32, ptr @W_cursor_hidden
%hdiff = icmp ne i32 %ch, %wh
br i1 %hdiff, label %doh, label %skiph
doh:
%curscls = call ptr @objc_getClass(ptr @.c_cursor)
%wanth = icmp ne i32 %wh, 0
br i1 %wanth, label %hide, label %unhide
hide:
%selhide = call ptr @sel_registerName(ptr @.s_hide)
%r1 = call ptr (ptr, ptr) @objc_msgSend(ptr %curscls, ptr %selhide)
br label %storeh
unhide:
%selunh = call ptr @sel_registerName(ptr @.s_unhide)
%r2 = call ptr (ptr, ptr) @objc_msgSend(ptr %curscls, ptr %selunh)
br label %storeh
storeh:
store i32 %wh, ptr @W_cursor_hidden
br label %skiph
skiph:
%ca = load i32, ptr @W_cursor_assoc
%adiff = icmp ne i32 %ca, %wa
br i1 %adiff, label %doa, label %skipa
doa:
%rc = call i32 @CGAssociateMouseAndMouseCursorPosition(i32 %wa)
store i32 %wa, ptr @W_cursor_assoc
br label %skipa
skipa:
ret void
}
; Re-derive the wanted state from the mode + window key status, then apply it.
; Called on a mode change and once per frame from win_poll, so losing focus
; (Cmd-Tab) auto-releases the cursor and refocusing re-acquires it.
define void @win_cursor_maintain() {
entry:
%mode = load i32, ptr @W_cursor_mode
%win = load ptr, ptr @W_win
%nowin = icmp eq ptr %win, null
br i1 %nowin, label %release, label %askkey
askkey:
%selk = call ptr @sel_registerName(ptr @.s_iskey)
%k8 = call i8 (ptr, ptr) @objc_msgSend(ptr %win, ptr %selk)
%iskey = icmp ne i8 %k8, 0
br i1 %iskey, label %keyed, label %release
release:
; not focused (or no window): show the cursor and reconnect it
call void @cursor_apply(i32 0, i32 1)
ret void
keyed:
; want_hidden when mode is 1 (hidden) or 2 (locked)
%h1 = icmp eq i32 %mode, 1
%h2 = icmp eq i32 %mode, 2
%wh = or i1 %h1, %h2
%whi = zext i1 %wh to i32
; dissociate (relative) when mode is 2 (locked) or 3 (confined)
%a2 = icmp eq i32 %mode, 2
%a3 = icmp eq i32 %mode, 3
%dis = or i1 %a2, %a3
%wa = xor i1 %dis, true
%wai = zext i1 %wa to i32
call void @cursor_apply(i32 %whi, i32 %wai)
ret void
}
; the Ludic entry point: set the desired cursor mode and apply it now.
define void @win_cursor_mode(i32 %mode) {
entry:
store i32 %mode, ptr @W_cursor_mode
call void @win_cursor_maintain()
ret void
}
; ============================================================================
; #51 — gamepad, via GameController.framework.
;