Skip to main content

input/
capforge.rs

1//! Capability bitmap kernel shared by ioctl and sysfs discovery paths.
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4#[repr(i32)]
5pub enum CapabilityKind {
6    Unknown = 0,
7    Keyboard = 1,
8    Key = 2,
9    Mouse = 3,
10    Touchpad = 4,
11    Touchscreen = 5,
12    Tablet = 6,
13    Joystick = 7,
14    Switch = 8,
15}
16
17impl CapabilityKind {
18    fn from_code(code: i32) -> Self {
19        match code {
20            1 => Self::Keyboard,
21            2 => Self::Key,
22            3 => Self::Mouse,
23            4 => Self::Touchpad,
24            5 => Self::Touchscreen,
25            6 => Self::Tablet,
26            7 => Self::Joystick,
27            8 => Self::Switch,
28            _ => Self::Unknown,
29        }
30    }
31}
32
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub struct CapabilityBits {
35    ev: [i64; 1],
36    key: [i64; 12],
37    rel: [i64; 1],
38    absolute: [i64; 2],
39    properties: [i64; 1],
40}
41
42impl CapabilityBits {
43    pub fn from_sysfs_event_node(devnode: &std::path::Path) -> Self {
44        let Some(name) = devnode.file_name() else {
45            return Self::default();
46        };
47        let device = std::path::Path::new("/sys/class/input")
48            .join(name)
49            .join("device");
50        let capabilities = device.join("capabilities");
51        let read =
52            |name: &str| std::fs::read_to_string(capabilities.join(name)).unwrap_or_default();
53        Self {
54            ev: parse_sysfs_hex::<1>(&read("ev")),
55            key: parse_sysfs_hex::<12>(&read("key")),
56            rel: parse_sysfs_hex::<1>(&read("rel")),
57            absolute: parse_sysfs_hex::<2>(&read("abs")),
58            properties: parse_sysfs_hex::<1>(
59                &std::fs::read_to_string(device.join("properties")).unwrap_or_default(),
60            ),
61        }
62    }
63
64    pub fn set_event(&mut self, code: u16) {
65        set_bit(&mut self.ev, code);
66    }
67
68    pub fn set_key(&mut self, code: u16) {
69        set_bit(&mut self.key, code);
70    }
71
72    pub fn set_relative(&mut self, code: u16) {
73        set_bit(&mut self.rel, code);
74    }
75
76    pub fn set_absolute(&mut self, code: u16) {
77        set_bit(&mut self.absolute, code);
78    }
79
80    pub fn set_property(&mut self, code: u16) {
81        set_bit(&mut self.properties, code);
82    }
83
84    pub fn classify(&self) -> CapabilityKind {
85        native_classify(self)
86            .map(CapabilityKind::from_code)
87            .unwrap_or_else(|| rust_classify(self))
88    }
89
90    pub fn has_event(&self, code: u16) -> bool {
91        bit(&self.ev, code)
92    }
93
94    pub fn has_key(&self, code: u16) -> bool {
95        bit(&self.key, code)
96    }
97
98    pub fn has_relative(&self, code: u16) -> bool {
99        bit(&self.rel, code)
100    }
101
102    pub fn has_absolute(&self, code: u16) -> bool {
103        bit(&self.absolute, code)
104    }
105
106    pub fn key_count(&self) -> usize {
107        (0..=KEY_MAX).filter(|code| bit(&self.key, *code)).count()
108    }
109}
110
111const KEY_MAX: u16 = 0x2ff;
112
113fn set_bit(words: &mut [i64], code: u16) {
114    let index = usize::from(code / 64);
115    let offset = u32::from(code % 64);
116    if let Some(word) = words.get_mut(index) {
117        *word |= 1_i64.wrapping_shl(offset);
118    }
119}
120
121fn bit(words: &[i64], code: u16) -> bool {
122    let index = usize::from(code / 64);
123    let offset = u32::from(code % 64);
124    words
125        .get(index)
126        .is_some_and(|word| (word.wrapping_shr(offset) & 1) == 1)
127}
128
129fn rust_classify(bits: &CapabilityBits) -> CapabilityKind {
130    let has_key = bit(&bits.ev, 1);
131    let has_rel = bit(&bits.ev, 2);
132    let has_abs = bit(&bits.ev, 3);
133    let xy = has_abs && bit(&bits.absolute, 0) && bit(&bits.absolute, 1);
134    let multitouch = bit(&bits.absolute, 0x2f) || bit(&bits.absolute, 0x35);
135    let finger = bit(&bits.key, 0x145);
136    let touch = bit(&bits.key, 0x14a);
137    let pen = bit(&bits.key, 0x140);
138    let left = bit(&bits.key, 0x110);
139    let joystick = bit(&bits.key, 0x120);
140    let relative_xy = has_rel && (bit(&bits.rel, 0) || bit(&bits.rel, 1));
141    let direct = bit(&bits.properties, 1);
142    let pointer = bit(&bits.properties, 0);
143
144    if pen && xy {
145        CapabilityKind::Tablet
146    } else if (finger || (touch && pointer && !direct)) && xy {
147        CapabilityKind::Touchpad
148    } else if (direct || (touch && multitouch)) && xy {
149        CapabilityKind::Touchscreen
150    } else if relative_xy && left {
151        CapabilityKind::Mouse
152    } else if joystick && has_abs {
153        CapabilityKind::Joystick
154    } else if has_key {
155        let count = (1..255).filter(|code| bit(&bits.key, *code)).count();
156        if count > 20 {
157            CapabilityKind::Keyboard
158        } else if count > 0 {
159            CapabilityKind::Key
160        } else {
161            CapabilityKind::Unknown
162        }
163    } else if bit(&bits.ev, 5) {
164        CapabilityKind::Switch
165    } else {
166        CapabilityKind::Unknown
167    }
168}
169
170pub fn parse_sysfs_hex<const WORDS: usize>(input: &str) -> [i64; WORDS] {
171    let mut words = [0_i64; WORDS];
172    if native_parse(input, &mut words) {
173        return words;
174    }
175    for (index, token) in input.split_whitespace().rev().take(WORDS).enumerate() {
176        words[index] = u64::from_str_radix(token, 16).unwrap_or(0) as i64;
177    }
178    words
179}
180
181pub fn knn_scores(features: &[f64], centroids: &[f64], profiles: usize) -> Vec<f64> {
182    if features.is_empty() || profiles == 0 || centroids.len() != features.len() * profiles {
183        return Vec::new();
184    }
185    let mut scores = vec![0.0; profiles];
186    if native_knn_scores(features, centroids, &mut scores) {
187        return scores;
188    }
189    for (score, centroid) in scores
190        .iter_mut()
191        .zip(centroids.chunks_exact(features.len()))
192    {
193        *score = -features
194            .iter()
195            .zip(centroid)
196            .map(|(feature, center)| (feature - center).powi(2))
197            .sum::<f64>();
198    }
199    scores
200}
201
202pub fn tiny_mlp_scores(
203    features: &[f64],
204    input_weights: &[f64],
205    hidden_bias: &[f64],
206    output_weights: &[f64],
207    output_bias: &[f64],
208) -> Vec<f64> {
209    if features.is_empty()
210        || hidden_bias.is_empty()
211        || output_bias.is_empty()
212        || input_weights.len() != features.len() * hidden_bias.len()
213        || output_weights.len() != hidden_bias.len() * output_bias.len()
214    {
215        return Vec::new();
216    }
217    let mut scores = vec![0.0; output_bias.len()];
218    if native_mlp_scores(
219        features,
220        input_weights,
221        hidden_bias,
222        output_weights,
223        output_bias,
224        &mut scores,
225    ) {
226        return scores;
227    }
228    let hidden = input_weights
229        .chunks_exact(features.len())
230        .zip(hidden_bias)
231        .map(|(weights, bias)| {
232            (bias
233                + weights
234                    .iter()
235                    .zip(features)
236                    .map(|(weight, feature)| weight * feature)
237                    .sum::<f64>())
238            .tanh()
239        })
240        .collect::<Vec<_>>();
241    for ((score, weights), bias) in scores
242        .iter_mut()
243        .zip(output_weights.chunks_exact(hidden.len()))
244        .zip(output_bias)
245    {
246        *score = bias
247            + weights
248                .iter()
249                .zip(&hidden)
250                .map(|(weight, value)| weight * value)
251                .sum::<f64>();
252    }
253    scores
254}
255
256include!(concat!(env!("OUT_DIR"), "/capforge_bindings.rs"));
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn assert_native_matches(bits: &CapabilityBits, expected: CapabilityKind) {
263        assert_eq!(rust_classify(bits), expected);
264        if NATIVE_CAPFORGE {
265            assert_eq!(bits.classify(), expected);
266        }
267    }
268
269    #[test]
270    fn sysfs_words_are_reversed_into_ioctl_order() {
271        let words = parse_sysfs_hex::<3>("8000000000000000 20 1\n");
272        assert_eq!(words[0] as u64, 1);
273        assert_eq!(words[1] as u64, 0x20);
274        assert_eq!(words[2] as u64, 0x8000_0000_0000_0000);
275    }
276
277    #[test]
278    fn classifiers_agree_on_touchpad_and_mixed_keyboard_evidence() {
279        let mut touchpad = CapabilityBits::default();
280        touchpad.set_event(1);
281        touchpad.set_event(3);
282        touchpad.set_absolute(0);
283        touchpad.set_absolute(1);
284        touchpad.set_key(0x145);
285        touchpad.set_key(0x14a);
286        touchpad.set_property(0);
287        assert_native_matches(&touchpad, CapabilityKind::Touchpad);
288
289        for key in 1..=30 {
290            touchpad.set_key(key);
291        }
292        assert_native_matches(&touchpad, CapabilityKind::Touchpad);
293    }
294
295    #[test]
296    fn classifiers_agree_on_relative_pointer_and_switch() {
297        let mut mouse = CapabilityBits::default();
298        mouse.set_event(1);
299        mouse.set_event(2);
300        mouse.set_relative(0);
301        mouse.set_relative(1);
302        mouse.set_key(0x110);
303        assert_native_matches(&mouse, CapabilityKind::Mouse);
304
305        let mut switch = CapabilityBits::default();
306        switch.set_event(5);
307        assert_native_matches(&switch, CapabilityKind::Switch);
308    }
309
310    #[test]
311    fn native_and_rust_profile_scorers_agree() {
312        let features = [0.25, 0.5, 0.75];
313        let centroids = [0.0, 0.5, 1.0, 0.5, 0.5, 0.5];
314        let expected_knn = [-0.125, -0.125];
315        let actual_knn = knn_scores(&features, &centroids, 2);
316        for (actual, expected) in actual_knn.iter().zip(expected_knn) {
317            assert!((actual - expected).abs() < 1e-12);
318        }
319
320        let actual_mlp = tiny_mlp_scores(
321            &features,
322            &[1.0, 0.0, -1.0, -0.5, 1.0, 0.5],
323            &[0.1, -0.1],
324            &[0.5, -0.25, -0.75, 0.25],
325            &[0.0, 0.2],
326        );
327        assert_eq!(actual_mlp.len(), 2);
328        assert!(actual_mlp.iter().all(|score| score.is_finite()));
329    }
330}