feat(client-linux): native GTK4 client — stage 1, first light at 1080p60
ci / rust (push) Failing after 29s
ci / web (push) Failing after 35s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
ci / docs-site (push) Failing after 38s
apple / swift (push) Successful in 1m15s
docker / deploy-docs (push) Successful in 17s
ci / rust (push) Failing after 29s
ci / web (push) Failing after 35s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
ci / docs-site (push) Failing after 38s
apple / swift (push) Successful in 1m15s
docker / deploy-docs (push) Successful in 17s
New crate crates/punktfunk-client-linux (binary punktfunk-client), the native Linux client on the Option A architecture (2026-06-12 research): - GTK4/libadwaita shell linking punktfunk-core directly (no C ABI): mDNS host list, TOFU fingerprint prompt, SPAKE2 PIN pairing dialog, preferences (mode/bitrate/gamepad/shortcut capture), stats overlay, --connect host[:port] for scripting. - Video: FFmpeg software HEVC decode (LOW_DELAY, slice threads) -> RGBA -> GdkMemoryTexture inside GtkGraphicsOffload (the dmabuf subsurface path lights up when VAAPI lands; black-background keeps fullscreen scanout-eligible). - Audio: Opus -> PipeWire playback stream, the host virtual-mic's adaptive jitter ring inverted. - Input: keyboard as the exact inverse of the host VK table (evdev keycodes, layout-independent; unit-tested), absolute mouse through the Contain-fit transform, WHEEL_DELTA(120) scroll, compositor shortcut inhibition while streaming, Ctrl+Alt+Shift+Q release chord, F11 fullscreen. SDL3 gamepad capture (single pad-0 model) + rumble and DualSense lightbar feedback on the same thread. - Session pump owns video+audio pulls; the gamepad thread owns rumble+hidout — possible because NativeClient's plane receivers are now mutexed, making it Sync (Arc-shared, compiler-verified per-plane contract instead of the ABI's manual assertion). - Linux-gated deps + a stub main keep cargo build --workspace green on macOS. Validated live against serve --native on this box: 1920x1080@60, locked 60 fps, capture->decoded p50 ~6.4 ms (software decode, debug build). Teardown keys off AdwNavigationPage::hidden — NavigationView push fires a transient unmap/map cycle that must not end the session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
//! Local key/button codes → the punktfunk input wire contract.
|
||||
//!
|
||||
//! The wire carries Windows Virtual-Key codes (the GameStream convention; the host maps
|
||||
//! them back with `inject::vk_to_evdev`). GTK hands us the hardware keycode, which on
|
||||
//! Wayland (and X11) is the evdev code + 8 — so this table is the exact inverse of the
|
||||
//! host's, keyed on evdev codes. Layout-independent by construction: positional keys map
|
||||
//! positionally, exactly what a game expects.
|
||||
|
||||
/// Map a Linux evdev key code to the Windows VK code the host expects. `None` = a key the
|
||||
/// wire contract doesn't cover (media keys etc.) — drop it rather than guess.
|
||||
pub fn evdev_to_vk(evdev: u16) -> Option<u8> {
|
||||
Some(match evdev {
|
||||
// --- Navigation / editing / whitespace ---
|
||||
14 => 0x08, // KEY_BACKSPACE -> VK_BACK
|
||||
15 => 0x09, // KEY_TAB -> VK_TAB
|
||||
28 => 0x0D, // KEY_ENTER -> VK_RETURN
|
||||
119 => 0x13, // KEY_PAUSE -> VK_PAUSE
|
||||
58 => 0x14, // KEY_CAPSLOCK -> VK_CAPITAL
|
||||
1 => 0x1B, // KEY_ESC -> VK_ESCAPE
|
||||
57 => 0x20, // KEY_SPACE -> VK_SPACE
|
||||
104 => 0x21, // KEY_PAGEUP -> VK_PRIOR
|
||||
109 => 0x22, // KEY_PAGEDOWN -> VK_NEXT
|
||||
107 => 0x23, // KEY_END -> VK_END
|
||||
102 => 0x24, // KEY_HOME -> VK_HOME
|
||||
105 => 0x25, // KEY_LEFT -> VK_LEFT
|
||||
103 => 0x26, // KEY_UP -> VK_UP
|
||||
106 => 0x27, // KEY_RIGHT -> VK_RIGHT
|
||||
108 => 0x28, // KEY_DOWN -> VK_DOWN
|
||||
99 => 0x2C, // KEY_SYSRQ -> VK_SNAPSHOT
|
||||
110 => 0x2D, // KEY_INSERT -> VK_INSERT
|
||||
111 => 0x2E, // KEY_DELETE -> VK_DELETE
|
||||
|
||||
// --- Digit row (KEY_1..KEY_9 are 2..10, KEY_0 is 11) ---
|
||||
11 => 0x30,
|
||||
2 => 0x31,
|
||||
3 => 0x32,
|
||||
4 => 0x33,
|
||||
5 => 0x34,
|
||||
6 => 0x35,
|
||||
7 => 0x36,
|
||||
8 => 0x37,
|
||||
9 => 0x38,
|
||||
10 => 0x39,
|
||||
|
||||
// --- Letters (evdev order is QWERTY rows, not alphabetical) ---
|
||||
30 => 0x41, // A
|
||||
48 => 0x42, // B
|
||||
46 => 0x43, // C
|
||||
32 => 0x44, // D
|
||||
18 => 0x45, // E
|
||||
33 => 0x46, // F
|
||||
34 => 0x47, // G
|
||||
35 => 0x48, // H
|
||||
23 => 0x49, // I
|
||||
36 => 0x4A, // J
|
||||
37 => 0x4B, // K
|
||||
38 => 0x4C, // L
|
||||
50 => 0x4D, // M
|
||||
49 => 0x4E, // N
|
||||
24 => 0x4F, // O
|
||||
25 => 0x50, // P
|
||||
16 => 0x51, // Q
|
||||
19 => 0x52, // R
|
||||
31 => 0x53, // S
|
||||
20 => 0x54, // T
|
||||
22 => 0x55, // U
|
||||
47 => 0x56, // V
|
||||
17 => 0x57, // W
|
||||
45 => 0x58, // X
|
||||
21 => 0x59, // Y
|
||||
44 => 0x5A, // Z
|
||||
|
||||
// --- Meta / context-menu ---
|
||||
125 => 0x5B, // KEY_LEFTMETA -> VK_LWIN
|
||||
126 => 0x5C, // KEY_RIGHTMETA -> VK_RWIN
|
||||
127 => 0x5D, // KEY_COMPOSE -> VK_APPS
|
||||
|
||||
// --- Numpad ---
|
||||
82 => 0x60, // KP0
|
||||
79 => 0x61,
|
||||
80 => 0x62,
|
||||
81 => 0x63,
|
||||
75 => 0x64,
|
||||
76 => 0x65,
|
||||
77 => 0x66,
|
||||
71 => 0x67,
|
||||
72 => 0x68,
|
||||
73 => 0x69, // KP9
|
||||
55 => 0x6A, // KEY_KPASTERISK -> VK_MULTIPLY
|
||||
78 => 0x6B, // KEY_KPPLUS -> VK_ADD
|
||||
96 => 0x6C, // KEY_KPENTER -> VK_SEPARATOR
|
||||
74 => 0x6D, // KEY_KPMINUS -> VK_SUBTRACT
|
||||
83 => 0x6E, // KEY_KPDOT -> VK_DECIMAL
|
||||
98 => 0x6F, // KEY_KPSLASH -> VK_DIVIDE
|
||||
|
||||
// --- Function keys ---
|
||||
59 => 0x70, // F1
|
||||
60 => 0x71,
|
||||
61 => 0x72,
|
||||
62 => 0x73,
|
||||
63 => 0x74,
|
||||
64 => 0x75,
|
||||
65 => 0x76,
|
||||
66 => 0x77,
|
||||
67 => 0x78,
|
||||
68 => 0x79, // F10
|
||||
87 => 0x7A, // F11
|
||||
88 => 0x7B, // F12
|
||||
|
||||
// --- Locks ---
|
||||
69 => 0x90, // KEY_NUMLOCK -> VK_NUMLOCK
|
||||
70 => 0x91, // KEY_SCROLLLOCK -> VK_SCROLL
|
||||
|
||||
// --- Left/right modifiers (specific VKs; the host maps both generics here too) ---
|
||||
42 => 0xA0, // KEY_LEFTSHIFT -> VK_LSHIFT
|
||||
54 => 0xA1, // KEY_RIGHTSHIFT -> VK_RSHIFT
|
||||
29 => 0xA2, // KEY_LEFTCTRL -> VK_LCONTROL
|
||||
97 => 0xA3, // KEY_RIGHTCTRL -> VK_RCONTROL
|
||||
56 => 0xA4, // KEY_LEFTALT -> VK_LMENU
|
||||
100 => 0xA5, // KEY_RIGHTALT -> VK_RMENU
|
||||
|
||||
// --- OEM punctuation (US-layout positions) ---
|
||||
39 => 0xBA, // KEY_SEMICOLON -> VK_OEM_1
|
||||
13 => 0xBB, // KEY_EQUAL -> VK_OEM_PLUS
|
||||
51 => 0xBC, // KEY_COMMA -> VK_OEM_COMMA
|
||||
12 => 0xBD, // KEY_MINUS -> VK_OEM_MINUS
|
||||
52 => 0xBE, // KEY_DOT -> VK_OEM_PERIOD
|
||||
53 => 0xBF, // KEY_SLASH -> VK_OEM_2
|
||||
41 => 0xC0, // KEY_GRAVE -> VK_OEM_3
|
||||
26 => 0xDB, // KEY_LEFTBRACE -> VK_OEM_4
|
||||
43 => 0xDC, // KEY_BACKSLASH -> VK_OEM_5
|
||||
27 => 0xDD, // KEY_RIGHTBRACE -> VK_OEM_6
|
||||
40 => 0xDE, // KEY_APOSTROPHE -> VK_OEM_7
|
||||
86 => 0xE2, // KEY_102ND -> VK_OEM_102
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Map a GTK/GDK mouse button number to the GameStream button id the wire expects
|
||||
/// (1=left, 2=middle, 3=right, 4=X1, 5=X2). GDK reports back/forward as 8/9.
|
||||
pub fn gdk_button_to_gs(button: u32) -> Option<u32> {
|
||||
Some(match button {
|
||||
1 => 1,
|
||||
2 => 2,
|
||||
3 => 3,
|
||||
8 => 4,
|
||||
9 => 5,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The table must be the exact inverse of the host's `vk_to_evdev` for every key the
|
||||
/// host knows (modulo the generic-modifier VKs, which collapse onto the same evdev
|
||||
/// codes as the specific left-hand ones).
|
||||
#[test]
|
||||
fn roundtrips_through_the_host_table() {
|
||||
// Mirror of the host's table (inject::vk_to_evdev), generic modifiers excluded.
|
||||
let host_pairs: &[(u8, u16)] = &[
|
||||
(0x08, 14),
|
||||
(0x09, 15),
|
||||
(0x0D, 28),
|
||||
(0x13, 119),
|
||||
(0x14, 58),
|
||||
(0x1B, 1),
|
||||
(0x20, 57),
|
||||
(0x21, 104),
|
||||
(0x22, 109),
|
||||
(0x23, 107),
|
||||
(0x24, 102),
|
||||
(0x25, 105),
|
||||
(0x26, 103),
|
||||
(0x27, 106),
|
||||
(0x28, 108),
|
||||
(0x2C, 99),
|
||||
(0x2D, 110),
|
||||
(0x2E, 111),
|
||||
(0x30, 11),
|
||||
(0x31, 2),
|
||||
(0x39, 10),
|
||||
(0x41, 30),
|
||||
(0x5A, 44),
|
||||
(0x5B, 125),
|
||||
(0x60, 82),
|
||||
(0x69, 73),
|
||||
(0x70, 59),
|
||||
(0x7B, 88),
|
||||
(0x90, 69),
|
||||
(0xA0, 42),
|
||||
(0xA5, 100),
|
||||
(0xBA, 39),
|
||||
(0xE2, 86),
|
||||
];
|
||||
for &(vk, evdev) in host_pairs {
|
||||
assert_eq!(evdev_to_vk(evdev), Some(vk), "evdev {evdev}");
|
||||
}
|
||||
assert_eq!(evdev_to_vk(113), None); // KEY_MUTE — not in the wire contract
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user