From 0959427f6404306077a547d757dad8328478809a Mon Sep 17 00:00:00 2001 From: Steven Liekens Date: Mon, 6 Jul 2026 15:42:57 +0200 Subject: [PATCH 1/3] Add experimental reMarkable 1/rM2 windowed build (rm1 feature) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional armv7 qtfb/AppLoad target alongside unchanged default Paper Pro builds. Gated by --features rm1: 1404x1872 display, Wacom pen + axis map, qtfb-shim launch script. qtfb/evdev wire layouts follow target pointer width (32 vs 64 bit). Windowed mode also falls back to qtfb pen events when evdev grab fails or AppLoad input shim is active — safe for existing aarch64 builds. --- .gitignore | 1 + README.md | 33 +++++++++-- riddle/.cargo/config.toml | 3 + riddle/Cargo.toml | 6 +- riddle/build-rm1.sh | 21 +++++++ riddle/external.manifest.rm1.json | 6 ++ riddle/scripts/appload-launch-rm1.sh | 18 ++++++ riddle/src/display.rs | 17 +++--- riddle/src/fb.rs | 7 +++ riddle/src/main.rs | 11 +++- riddle/src/pen.rs | 84 +++++++++++++++++++++++----- riddle/src/qtfb.rs | 71 ++++++++++++++++++----- 12 files changed, 235 insertions(+), 43 deletions(-) create mode 100644 riddle/build-rm1.sh create mode 100644 riddle/external.manifest.rm1.json create mode 100644 riddle/scripts/appload-launch-rm1.sh diff --git a/.gitignore b/.gitignore index 0f2fb28..51bce10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ riddle/target/ +riddle/dist-rm1/ quill/build/ quill/vendor/ *.log diff --git a/README.md b/README.md index b5fd330..4e15acb 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,13 @@ Already have xovi + AppLoad? **[Download the latest release](https://github.com/ 4. In **AppLoad**: tap **Reload**, then **The Diary**. Write, and rest your pen. > ⚠️ **This modifies your device.** It runs as root, stops the vendor UI -> (in takeover mode), and drives the e-ink engine directly. It has only been -> tested on a **reMarkable Paper Pro** (ferrari, aarch64, OS 3.26–3.27). It may -> not work on other models or OS versions, and you use it entirely at your own -> risk. Not affiliated with reMarkable AS. Keep SSH access working before you -> install anything — that is your escape hatch. +> (in takeover mode), and drives the e-ink engine directly. The Paper Pro +> build has been tested on **reMarkable Paper Pro** (ferrari, aarch64, OS +> 3.26–3.27). An experimental **reMarkable 1 / rM2** windowed build also exists +> (see [below](#windowed-rm1--rm2-experimental)); it may not work on other models +> or OS versions, and you use it entirely at your own risk. Not affiliated with +> reMarkable AS. Keep SSH access working before you install anything — that is +> your escape hatch. ## How it works @@ -128,6 +130,27 @@ cargo build --release --target aarch64-unknown-linux-gnu Install to `/home/root/xovi/exthome/appload/riddle/` with `external.manifest.json`, `appload-launch.sh`, and the binary. +### Windowed (rM1 / rM2, experimental) + +reMarkable 1 and rM2 (1404×1872, armv7, Wacom digitizer) can run the diary in +**qtfb windowed mode only** — no `quill` takeover. Build with the `rm1` feature +(default Paper Pro builds omit it): + +```sh +cd riddle +./build-rm1.sh # → dist-rm1/ +``` + +Copy `dist-rm1/` to `/home/root/xovi/exthome/appload/riddle/`. Use the bundled +`external.manifest.json` (from `external.manifest.rm1.json`). The included +`appload-launch-rm1.sh` preloads AppLoad's `qtfb-shim-32bit.so` so stylus input +reaches the app while xochitl owns the real Wacom device. + +Requires [xovi + AppLoad](https://github.com/asivery/rm-appload) **arm32**. +AppLoad **v0.4.2** is known-good on OS 3.22; newer hooks may crash on older OS +builds. Gestures: write, flip marker to erase, large **?** for help; close the +AppLoad window to quit (no 5-finger or power-button sleep in this mode). + ### Takeover (instant ink) — the one from the demo Requires the reMarkable SDK toolchain (`~/rm-sdk-3.26`) because the linked diff --git a/riddle/.cargo/config.toml b/riddle/.cargo/config.toml index 3c32d25..beb596d 100644 --- a/riddle/.cargo/config.toml +++ b/riddle/.cargo/config.toml @@ -1,2 +1,5 @@ [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" diff --git a/riddle/Cargo.toml b/riddle/Cargo.toml index 803aa0c..a6a1bcb 100644 --- a/riddle/Cargo.toml +++ b/riddle/Cargo.toml @@ -3,7 +3,7 @@ name = "riddle" version = "0.1.0" edition = "2021" license = "MIT" -description = "The diary of Tom Riddle, for the reMarkable Paper Pro" +description = "The diary of Tom Riddle, for the reMarkable Paper Pro (optional rM1/rM2 windowed build)" [dependencies] libc = "0.2" @@ -15,6 +15,10 @@ ab_glyph = "0.2" ureq = { version = "2.10", default-features = false, features = ["tls"] } [features] +default = [] +# reMarkable 1 / 2 (armv7, 1404x1872, Wacom digitizer, qtfb windowed only). +# Default builds (no feature) are unchanged Paper Pro / aarch64 targets. +rm1 = [] # Link libquill.so (vendor e-ink engine C ABI) for full-takeover mode. takeover = [] diff --git a/riddle/build-rm1.sh b/riddle/build-rm1.sh new file mode 100644 index 0000000..d5c92a7 --- /dev/null +++ b/riddle/build-rm1.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Cross-build riddle for reMarkable 1/2 (armv7, windowed AppLoad/qtfb). +set -euo pipefail +cd "$(dirname "$0")" + +export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc + +cargo build --release --target armv7-unknown-linux-gnueabihf --no-default-features --features rm1 + +OUT=dist-rm1 +rm -rf "$OUT" +mkdir -p "$OUT" + +cp target/armv7-unknown-linux-gnueabihf/release/riddle "$OUT/riddle" +cp external.manifest.rm1.json "$OUT/external.manifest.json" +cp scripts/appload-launch-rm1.sh "$OUT/appload-launch-rm1.sh" +cp oracle.env.example "$OUT/oracle.env.example" +cp icon.png "$OUT/icon.png" +chmod +x "$OUT/riddle" "$OUT/appload-launch-rm1.sh" + +echo "Built $OUT/ — copy to the tablet with setup-rm1.ps1 or setup-rm1.sh" \ No newline at end of file diff --git a/riddle/external.manifest.rm1.json b/riddle/external.manifest.rm1.json new file mode 100644 index 0000000..a9de61b --- /dev/null +++ b/riddle/external.manifest.rm1.json @@ -0,0 +1,6 @@ +{ + "name": "The Diary", + "application": "appload-launch-rm1.sh", + "qtfb": true, + "aspectRatio": "original" +} \ No newline at end of file diff --git a/riddle/scripts/appload-launch-rm1.sh b/riddle/scripts/appload-launch-rm1.sh new file mode 100644 index 0000000..0640ce9 --- /dev/null +++ b/riddle/scripts/appload-launch-rm1.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# AppLoad entry point for reMarkable 1/2 (windowed qtfb mode). +HERE=$(cd "$(dirname "$0")" && pwd) +SHIM=/home/root/xovi/exthome/appload/shims/qtfb-shim-32bit.so +if [ -f "$HERE/oracle.env" ]; then + set -a + . "$HERE/oracle.env" + set +a +fi +# Route stylus through AppLoad's qtfb input shim (xochitl owns the real Wacom). +if [ -f "$SHIM" ]; then + export LD_PRELOAD="$SHIM" + export QTFB_SHIM_FB=0 + export QTFB_SHIM_INPUT=1 + export QTFB_SHIM_MODEL=RM1 + export QTFB_SHIM_INPUT_MODE=RM1 +fi +exec "$HERE/riddle" \ No newline at end of file diff --git a/riddle/src/display.rs b/riddle/src/display.rs index 7ca77e5..ec2ca3c 100644 --- a/riddle/src/display.rs +++ b/riddle/src/display.rs @@ -29,17 +29,18 @@ impl Display { pub fn open() -> io::Result<(Self, Surface)> { if let Ok(key) = std::env::var("QTFB_KEY") { let key: i32 = key.parse().map_err(io::Error::other)?; - let mut client = crate::qtfb::QtfbClient::connect( - key, - crate::qtfb::FBFMT_RMPP_RGB565, - 1620, - 2160, - 2, - )?; + #[cfg(feature = "rm1")] + let (fmt, w, h) = (crate::qtfb::FBFMT_RM2FB, 1404, 1872); + #[cfg(not(feature = "rm1"))] + let (fmt, w, h) = (crate::qtfb::FBFMT_RMPP_RGB565, 1620, 2160); + let mut client = crate::qtfb::QtfbClient::connect(key, fmt, w, h, 2)?; + #[cfg(feature = "rm1")] + let _ = client.set_refresh_mode(crate::qtfb::REFRESH_MODE_FAST); + #[cfg(not(feature = "rm1"))] let _ = client.set_refresh_mode(crate::qtfb::REFRESH_MODE_UFAST); let buf = client.framebuffer(); let (ptr, len) = (buf.as_mut_ptr(), buf.len()); - let surface = Surface::new(ptr, len, 1620, 2160, 1620 * 2, PixFmt::Rgb565); + let surface = Surface::new(ptr, len, w, h, w * 2, PixFmt::Rgb565); return Ok((Display::Qtfb(client), surface)); } diff --git a/riddle/src/fb.rs b/riddle/src/fb.rs index 0a596af..6cc5098 100644 --- a/riddle/src/fb.rs +++ b/riddle/src/fb.rs @@ -1,8 +1,15 @@ //! Geometry helpers. Drawing lives in surface.rs. +#[cfg(not(feature = "rm1"))] pub const SCREEN_W: usize = 1620; +#[cfg(not(feature = "rm1"))] pub const SCREEN_H: usize = 2160; +#[cfg(feature = "rm1")] +pub const SCREEN_W: usize = 1404; +#[cfg(feature = "rm1")] +pub const SCREEN_H: usize = 1872; + /// Grow-only pixel bounding box, used to build update/dissolve regions. #[derive(Clone, Copy, Debug)] pub struct BBox { diff --git a/riddle/src/main.rs b/riddle/src/main.rs index 6d3d8ea..58e37a0 100644 --- a/riddle/src/main.rs +++ b/riddle/src/main.rs @@ -119,8 +119,15 @@ fn run() -> std::io::Result<()> { surf.stride ); + let input_shim = std::env::var("LD_PRELOAD") + .map(|v| v.contains("qtfb-shim")) + .unwrap_or(false); let mut pen_dev = match pen::PenDevice::open() { - Ok(p) => Some(p), + Ok(p) if p.is_grabbed() || input_shim => Some(p), + Ok(_) => { + eprintln!("riddle: pen grab failed, using qtfb pen events"); + None + } Err(e) => { eprintln!("riddle: raw pen unavailable ({e}), falling back to qtfb pen events"); None @@ -243,7 +250,7 @@ fn run() -> std::io::Result<()> { // ---- raw pen (preferred path) ---- if let Some(ref mut pdev) = pen_dev { for s in pdev.drain() { - let writing = s.touching && s.pressure > 40; + let writing = s.pressure > 40 || (s.touching && s.pressure > 0); stylus_on = writing; stylus_tapped |= writing; if !writing { diff --git a/riddle/src/pen.rs b/riddle/src/pen.rs index 91cf6b6..fe2adbc 100644 --- a/riddle/src/pen.rs +++ b/riddle/src/pen.rs @@ -10,11 +10,21 @@ use std::os::fd::RawFd; use crate::fb::{SCREEN_H, SCREEN_W}; -// Digitizer axis ranges on the Paper Pro ("Elan marker input"). +// Digitizer axis ranges — Paper Pro uses "Elan marker input"; rM1/rM2 use Wacom. +#[cfg(not(feature = "rm1"))] const DIGI_MAX_X: i32 = 11180; +#[cfg(not(feature = "rm1"))] const DIGI_MAX_Y: i32 = 15340; +#[cfg(not(feature = "rm1"))] pub const MAX_PRESSURE: i32 = 4096; +#[cfg(feature = "rm1")] +const DIGI_MAX_X: i32 = 20967; +#[cfg(feature = "rm1")] +const DIGI_MAX_Y: i32 = 15725; +#[cfg(feature = "rm1")] +pub const MAX_PRESSURE: i32 = 4095; + const EV_SYN: u16 = 0; const EV_KEY: u16 = 1; const EV_ABS: u16 = 3; @@ -47,6 +57,7 @@ pub struct PenSample { pub struct PenDevice { fd: RawFd, + grabbed: bool, // Accumulated state between SYN_REPORTs. raw_x: i32, raw_y: i32, @@ -69,9 +80,11 @@ impl PenDevice { if grab != 0 { eprintln!("riddle: warning: EVIOCGRAB failed ({}) — xochitl will also see the pen", io::Error::last_os_error()); } - eprintln!("riddle: pen device {path} opened (grabbed: {})", grab == 0); + let grabbed = grab == 0; + eprintln!("riddle: pen device {path} opened (grabbed: {grabbed})"); Ok(Self { fd, + grabbed, raw_x: 0, raw_y: 0, pressure: 0, @@ -85,21 +98,37 @@ impl PenDevice { self.fd } + pub fn is_grabbed(&self) -> bool { + self.grabbed + } + /// Drain all pending events; returns one sample per SYN_REPORT frame /// that changed state. pub fn drain(&mut self) -> Vec { let mut out = Vec::new(); - // input_event on 64-bit: struct timeval (16) + type u16 + code u16 + value i32. - let mut buf = [0u8; 24 * 64]; + #[cfg(target_pointer_width = "32")] + const EVENT_SIZE: usize = 16; + #[cfg(target_pointer_width = "64")] + const EVENT_SIZE: usize = 24; + let mut buf = [0u8; EVENT_SIZE * 64]; loop { let n = unsafe { libc::read(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }; if n <= 0 { break; } - for chunk in buf[..n as usize].chunks_exact(24) { - let etype = u16::from_le_bytes(chunk[16..18].try_into().unwrap()); - let code = u16::from_le_bytes(chunk[18..20].try_into().unwrap()); - let value = i32::from_le_bytes(chunk[20..24].try_into().unwrap()); + for chunk in buf[..n as usize].chunks_exact(EVENT_SIZE) { + #[cfg(target_pointer_width = "32")] + let (etype, code, value) = ( + u16::from_le_bytes(chunk[8..10].try_into().unwrap()), + u16::from_le_bytes(chunk[10..12].try_into().unwrap()), + i32::from_le_bytes(chunk[12..16].try_into().unwrap()), + ); + #[cfg(target_pointer_width = "64")] + let (etype, code, value) = ( + u16::from_le_bytes(chunk[16..18].try_into().unwrap()), + u16::from_le_bytes(chunk[18..20].try_into().unwrap()), + i32::from_le_bytes(chunk[20..24].try_into().unwrap()), + ); match (etype, code) { (EV_ABS, ABS_X) => { self.raw_x = value; @@ -126,12 +155,14 @@ impl PenDevice { (EV_SYN, SYN_REPORT) => { if self.dirty { self.dirty = false; + let touching = self.touching || self.pressure > 0; + let (x, y) = digi_to_screen(self.raw_x, self.raw_y); out.push(PenSample { - x: self.raw_x * (SCREEN_W as i32 - 1) / DIGI_MAX_X, - y: self.raw_y * (SCREEN_H as i32 - 1) / DIGI_MAX_Y, + x, + y, pressure: self.pressure, tool: self.tool, - touching: self.touching, + touching, }); } } @@ -152,14 +183,41 @@ impl Drop for PenDevice { } } +/// Map Wacom digitizer axes to portrait screen pixels. +/// rM1/rM2 ABS_X/ABS_Y are rotated vs the framebuffer; this inverts the +/// qtfb-shim translation (and matches the native kernel layout). +#[cfg(feature = "rm1")] +fn digi_to_screen(raw_x: i32, raw_y: i32) -> (i32, i32) { + let x = raw_y * (SCREEN_W as i32 - 1) / DIGI_MAX_Y; + let y = (DIGI_MAX_X - raw_x) * (SCREEN_H as i32 - 1) / DIGI_MAX_X; + (x.clamp(0, SCREEN_W as i32 - 1), y.clamp(0, SCREEN_H as i32 - 1)) +} + +#[cfg(not(feature = "rm1"))] +fn digi_to_screen(raw_x: i32, raw_y: i32) -> (i32, i32) { + ( + raw_x * (SCREEN_W as i32 - 1) / DIGI_MAX_X, + raw_y * (SCREEN_H as i32 - 1) / DIGI_MAX_Y, + ) +} + fn find_marker_device() -> io::Result { for i in 0..8 { let name_path = format!("/sys/class/input/event{i}/device/name"); if let Ok(name) = std::fs::read_to_string(&name_path) { - if name.to_lowercase().contains("marker") { + let lower = name.to_lowercase(); + #[cfg(feature = "rm1")] + let found = lower.contains("wacom"); + #[cfg(not(feature = "rm1"))] + let found = lower.contains("marker"); + if found { return Ok(format!("/dev/input/event{i}")); } } } - Err(io::Error::new(io::ErrorKind::NotFound, "no marker input device found")) + #[cfg(feature = "rm1")] + let msg = "no Wacom digitizer found"; + #[cfg(not(feature = "rm1"))] + let msg = "no marker input device found"; + Err(io::Error::new(io::ErrorKind::NotFound, msg)) } diff --git a/riddle/src/qtfb.rs b/riddle/src/qtfb.rs index daf5665..b4560ea 100644 --- a/riddle/src/qtfb.rs +++ b/riddle/src/qtfb.rs @@ -2,7 +2,8 @@ //! //! Wire format (verified against rm-appload src/qtfb/common.h): //! ClientMessage = 24 bytes, type:u8 @0, payload @4 -//! ServerMessage = 32 bytes, type:u8 @0, payload @8 +//! ServerMessage layout is ABI-dependent — aarch64 pads the union to @8, +//! armv7 places init/userInput fields immediately after the type byte @4. use std::io; use std::os::fd::RawFd; @@ -19,7 +20,9 @@ pub const MESSAGE_REQUEST_FULL_REFRESH: u8 = 6; pub const UPDATE_ALL: i32 = 0; pub const UPDATE_PARTIAL: i32 = 1; -/// FBFMT_RMPP_RGB565: native 1620x2160, 2 bytes/pixel, stride = 3240. +/// FBFMT_RM2FB: native 1404x1872 on rM1/rM2, 2 bytes/pixel RGB565. +pub const FBFMT_RM2FB: u8 = 0; +/// FBFMT_RMPP_RGB565: native 1620x2160 on Paper Pro, 2 bytes/pixel. pub const FBFMT_RMPP_RGB565: u8 = 3; #[allow(dead_code)] @@ -96,8 +99,7 @@ impl QtfbClient { msg[8] = format; send_all(fd, &msg)?; - // Init reply: shmKey i32 @8, shmSize u64 @16. Server closing without - // replying (recv == 0) means init was rejected. + // Init reply. Server closing without replying (recv == 0) means rejected. let mut reply = [0u8; 32]; let n = unsafe { libc::recv(fd, reply.as_mut_ptr() as *mut libc::c_void, 32, 0) }; if n <= 0 { @@ -107,8 +109,7 @@ impl QtfbClient { "qtfb server rejected init (no reply)", )); } - let shm_key = i32::from_le_bytes(reply[8..12].try_into().unwrap()); - let shm_size = u64::from_le_bytes(reply[16..24].try_into().unwrap()) as usize; + let (shm_key, shm_size) = parse_init_reply(&reply)?; let shm_path = format!("/dev/shm/qtfb_{}\0", shm_key); let shm_fd = unsafe { libc::open(shm_path.as_ptr() as *const libc::c_char, libc::O_RDWR) }; @@ -232,14 +233,8 @@ impl QtfbClient { } return Err(e); } - if buf[0] == MESSAGE_USERINPUT && n >= 28 { - out.push(InputEvent { - input_type: i32::from_le_bytes(buf[8..12].try_into().unwrap()), - dev_id: i32::from_le_bytes(buf[12..16].try_into().unwrap()), - x: i32::from_le_bytes(buf[16..20].try_into().unwrap()), - y: i32::from_le_bytes(buf[20..24].try_into().unwrap()), - d: i32::from_le_bytes(buf[24..28].try_into().unwrap()), - }); + if let Some(evt) = parse_userinput(&buf[..n as usize]) { + out.push(evt); } } } @@ -255,6 +250,54 @@ impl Drop for QtfbClient { } } +fn parse_init_reply(reply: &[u8; 32]) -> io::Result<(i32, usize)> { + // AppLoad's ServerMessage union is padded differently on 32- vs 64-bit ABIs. + #[cfg(target_pointer_width = "32")] + { + let shm_key = i32::from_le_bytes(reply[4..8].try_into().unwrap()); + let shm_size = u32::from_le_bytes(reply[8..12].try_into().unwrap()) as usize; + Ok((shm_key, shm_size)) + } + #[cfg(target_pointer_width = "64")] + { + let shm_key = i32::from_le_bytes(reply[8..12].try_into().unwrap()); + let shm_size = u64::from_le_bytes(reply[16..24].try_into().unwrap()) as usize; + Ok((shm_key, shm_size)) + } +} + +fn parse_userinput(buf: &[u8]) -> Option { + if buf.first().copied()? != MESSAGE_USERINPUT { + return None; + } + #[cfg(target_pointer_width = "32")] + { + if buf.len() < 24 { + return None; + } + Some(InputEvent { + input_type: i32::from_le_bytes(buf[4..8].try_into().ok()?), + dev_id: i32::from_le_bytes(buf[8..12].try_into().ok()?), + x: i32::from_le_bytes(buf[12..16].try_into().ok()?), + y: i32::from_le_bytes(buf[16..20].try_into().ok()?), + d: i32::from_le_bytes(buf[20..24].try_into().ok()?), + }) + } + #[cfg(target_pointer_width = "64")] + { + if buf.len() < 28 { + return None; + } + Some(InputEvent { + input_type: i32::from_le_bytes(buf[8..12].try_into().ok()?), + dev_id: i32::from_le_bytes(buf[12..16].try_into().ok()?), + x: i32::from_le_bytes(buf[16..20].try_into().ok()?), + y: i32::from_le_bytes(buf[20..24].try_into().ok()?), + d: i32::from_le_bytes(buf[24..28].try_into().ok()?), + }) + } +} + fn send_all(fd: RawFd, buf: &[u8]) -> io::Result<()> { loop { let n = unsafe { libc::send(fd, buf.as_ptr() as *const libc::c_void, buf.len(), 0) }; From 41400c229e62d8f3f8d8f9e83601c1d915439387 Mon Sep 17 00:00:00 2001 From: Steven Liekens Date: Mon, 6 Jul 2026 16:06:09 +0200 Subject: [PATCH 2/3] Fix stray ink in windowed qtfb mode (rM1/rM2) Disable takeover-only footstep stamps when running inside AppLoad; they read as random spatters on e-ink windowed builds. Tighten pen filtering: require BTN_TOUCH with pressure > 40, and ignore hover pressure noise. --- riddle/src/main.rs | 10 +++++----- riddle/src/pen.rs | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/riddle/src/main.rs b/riddle/src/main.rs index 58e37a0..e76fd4d 100644 --- a/riddle/src/main.rs +++ b/riddle/src/main.rs @@ -176,8 +176,8 @@ fn run() -> std::io::Result<()> { let mut stylus_on = false; let mut stylus_tapped = false; let mut ink_dirty = BBox::empty(); - // Experiment: while drawing, stamp a tiny faded footprint beside the ink. - // This tests mixing precomposed pixel art with live pen updates. + // Takeover-only experiment: stamp a faded footprint beside live ink (quill + // partial-update demo). Disabled in windowed qtfb — looks like stray spats. let mut last_footstep: Option<(i32, i32)> = None; let mut footstep_i: u32 = 0; let mut last_flush = Instant::now(); @@ -250,7 +250,7 @@ fn run() -> std::io::Result<()> { // ---- raw pen (preferred path) ---- if let Some(ref mut pdev) = pen_dev { for s in pdev.drain() { - let writing = s.pressure > 40 || (s.touching && s.pressure > 0); + let writing = s.touching && s.pressure > 40; stylus_on = writing; stylus_tapped |= writing; if !writing { @@ -271,7 +271,7 @@ fn run() -> std::io::Result<()> { pen::Tool::Pen => { let r = 2 + s.pressure * 3 / pen::MAX_PRESSURE; let mut d = user_ink.pen_point(&mut surf, s.x, s.y, r); - if should_stamp_footstep(last_footstep, s.x, s.y) { + if takeover && should_stamp_footstep(last_footstep, s.x, s.y) { let f = draw_faded_footstep(&mut surf, s.x + 52, s.y - 38, footstep_i); d.add(f.x0, f.y0, 0); d.add(f.x1, f.y1, 0); @@ -313,7 +313,7 @@ fn run() -> std::io::Result<()> { pen_down = true; let r = 2 + ev.d.clamp(0, 100) / 45; let mut d = user_ink.pen_point(&mut surf, ev.x, ev.y, r); - if should_stamp_footstep(last_footstep, ev.x, ev.y) { + if takeover && should_stamp_footstep(last_footstep, ev.x, ev.y) { let f = draw_faded_footstep(&mut surf, ev.x + 52, ev.y - 38, footstep_i); d.add(f.x0, f.y0, 0); d.add(f.x1, f.y1, 0); diff --git a/riddle/src/pen.rs b/riddle/src/pen.rs index fe2adbc..2f2f2ef 100644 --- a/riddle/src/pen.rs +++ b/riddle/src/pen.rs @@ -140,7 +140,9 @@ impl PenDevice { } (EV_ABS, ABS_PRESSURE) => { self.pressure = value; - self.dirty = true; + if self.touching { + self.dirty = true; + } } (EV_KEY, BTN_TOOL_PEN) if value == 1 => { self.tool = Tool::Pen; @@ -155,14 +157,13 @@ impl PenDevice { (EV_SYN, SYN_REPORT) => { if self.dirty { self.dirty = false; - let touching = self.touching || self.pressure > 0; let (x, y) = digi_to_screen(self.raw_x, self.raw_y); out.push(PenSample { x, y, pressure: self.pressure, tool: self.tool, - touching, + touching: self.touching, }); } } From 61f2f36ac9b3fba768340e602f1a65d9db099d6e Mon Sep 17 00:00:00 2001 From: Steven Liekens Date: Mon, 6 Jul 2026 16:14:16 +0200 Subject: [PATCH 3/3] Address PR review: chmod build scripts, guard qtfb init reply length - build-rm1.sh and appload-launch-rm1.sh: executable bit (100755) - qtfb connect: reject truncated init replies before parsing - main.rs: document why qtfb-shim path keeps pen_dev without EVIOCGRAB --- riddle/build-rm1.sh | 0 riddle/scripts/appload-launch-rm1.sh | 0 riddle/src/main.rs | 3 +++ riddle/src/qtfb.rs | 11 +++++++++++ 4 files changed, 14 insertions(+) mode change 100644 => 100755 riddle/build-rm1.sh mode change 100644 => 100755 riddle/scripts/appload-launch-rm1.sh diff --git a/riddle/build-rm1.sh b/riddle/build-rm1.sh old mode 100644 new mode 100755 diff --git a/riddle/scripts/appload-launch-rm1.sh b/riddle/scripts/appload-launch-rm1.sh old mode 100644 new mode 100755 diff --git a/riddle/src/main.rs b/riddle/src/main.rs index e76fd4d..91fc1b0 100644 --- a/riddle/src/main.rs +++ b/riddle/src/main.rs @@ -119,6 +119,9 @@ fn run() -> std::io::Result<()> { surf.stride ); + // AppLoad's qtfb-shim intercepts the digitizer open() and feeds pen events + // from the qtfb socket into a virtual evdev fd. EVIOCGRAB fails on that fd + // even though it is the correct input path (tested on rM1 OS 3.22). let input_shim = std::env::var("LD_PRELOAD") .map(|v| v.contains("qtfb-shim")) .unwrap_or(false); diff --git a/riddle/src/qtfb.rs b/riddle/src/qtfb.rs index b4560ea..0f8b6f9 100644 --- a/riddle/src/qtfb.rs +++ b/riddle/src/qtfb.rs @@ -109,6 +109,17 @@ impl QtfbClient { "qtfb server rejected init (no reply)", )); } + #[cfg(target_pointer_width = "32")] + let min_reply = 12; + #[cfg(target_pointer_width = "64")] + let min_reply = 24; + if (n as usize) < min_reply { + unsafe { libc::close(fd) }; + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("qtfb init reply too short: {n} < {min_reply}"), + )); + } let (shm_key, shm_size) = parse_init_reply(&reply)?; let shm_path = format!("/dev/shm/qtfb_{}\0", shm_key);