Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLEANROOM.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ history as part of the provenance record.

- The clean-room replacement specification supplied by the project owner
- Qt 6 headers from the matching reMarkable SDK
- The matching official reMarkable SDK artifacts and developer documentation
- ELF metadata and dynamic-linking behavior
- `libqsgepaper.so` obtained by the device owner and not redistributed
- Black-box behavior of the owner's reMarkable Paper Pro
- Black-box behavior of the owner's reMarkable 2 and reMarkable Paper Pro

## Verification boundary

Expand Down
73 changes: 49 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# quill

Takeover display host for the reMarkable Paper Pro: stops xochitl and drives
the e-ink panel directly through the vendor waveform engine
Takeover display host for the reMarkable 2 and reMarkable Paper Pro: stops
xochitl and drives the e-ink panel through the vendor waveform engine
(`libqsgepaper`'s `EPFramebuffer`), with raw evdev input.

This is the lowest-latency third-party drawing path that exists on this device
short of reverse-engineering the FPGA transport frame format.
Quill is designed as a low-latency third-party drawing path without
reverse-engineering the FPGA transport frame format.

Verified targets:

- **reMarkable 2** — ARMv7 hard-float, monochrome SWTCON backend, reMarkable OS
3.27.3.0 with the 3.27.0.97 / 5.7.119 RM2 SDK.
- **reMarkable Paper Pro** — AArch64, monochrome and color ACEP backend,
reMarkable OS 3.27.3.0.

Quill supports two verified update classes on the Paper Pro:

Expand All @@ -15,14 +22,18 @@ Quill supports two verified update classes on the Paper Pro:
region updates. Mode `1` was observed to collapse to grayscale even with
`EPContentType::Color`.

Color is written through the captured RGB32 aux framebuffer (`B,G,R,0xFF`) and
pushed with `quill_swap_ex(..., QUILL_CONTENT_COLOR)` or the semantic wrappers
in `src/quill.h`.
On the Paper Pro, color is written through the captured RGB32 aux framebuffer
(`B,G,R,0xFF`) and pushed with `quill_swap_ex(..., QUILL_CONTENT_COLOR)` or the
semantic wrappers in `src/quill.h`. Monochrome clients also support the
vendor's RGB16 framebuffer variant with correctly packed RGB565 values.

- `src/vendor_probe.cpp`, `src/vendor_probe.h` — clean-room Qt/vendor boundary
and controlled framebuffer discovery
- `src/quill.h`, `src/quill_c.cpp` — C ABI over the engine (init/buffer/swap)
for C and Rust apps, including semantic mono/color swap wrappers
- `src/quill.h`, `src/quill_c.cpp`, `src/quill_surface.h` — C ABI over the
engine, its model-aware surface/profile, pixel helpers, and semantic
mono/color swap wrappers
- `src/input_names.h`, `src/pen_transform.h` — portable input-role matching and
model-aware pen-coordinate transforms
- `src/scribble.c` — C1 milestone: pen-to-glass latency demo (exit: pen
side-button in hover, 5-finger tap, power button, or SIGTERM)
- `examples/vanishing_trail.c` — archived no-AI drawing experiment demonstrating live ink and delayed erasure
Expand All @@ -31,35 +42,41 @@ in `src/quill.h`.
`medium-map.gif` / `deviant-map.gif` are recordings of it running)
- `src/image_demo.cpp` — render a PNG/JPEG/etc. image through Qt's QImage
loader, scaled to the panel
- `src/color_probe.c` — full-screen RGB/CMY color path probe
- `src/color_probe.c` — Paper Pro full-screen RGB/CMY color path probe
- `src/color_mode_compare.c` — side-by-side mode comparison (`1`, `3`, `4`, `5`)
- `src/color_partial_probe.c` — small dirty-rect color add/erase/churn probe
- `src/color_blend_probe.c` — software alpha, color mixing, and stacked partial
color updates
- `src/image_anim_demo.cpp`, `src/gif_demo.cpp` — partial-update animation
experiments (sprites over a still image; GIF playback)
- `scripts/takeover.sh` — stop xochitl, run app, ALWAYS restore xochitl
- `build.sh` — cross-build against the ferrari SDK (~/rm-sdk-3.26) +
`vendor/libqsgepaper.so` pulled from the device (the SDK comes from
reMarkable's developer program; build.sh expects it unpacked at
`~/rm-sdk-3.26` and the tablet reachable over ssh to fetch the vendor lib)
- `build.sh` — cross-build against the matching official RM2 or ferrari SDK;
current SDKs include the target's `libqsgepaper.so`, with `vendor/` retained
as a fallback for older SDKs

Exit the demos: power button, 5-finger tap, or SIGTERM.

The RM2 target currently builds the core adapter and device probes only. The
interactive demos in this branch retain their Paper Pro input and pixel
assumptions.

## Build and verification

The official SDK requires Linux. Set `QUILL_SDK` to the installed SDK root.

```sh
./build.sh # cross-build with the reMarkable SDK
./scripts/test-host.sh # clipping tests with ASan and UBSan
QUILL_SDK=/path/to/rm2-sdk ./build.sh rm2
# or: QUILL_SDK=/path/to/paper-pro-sdk ./build.sh ferrari

./scripts/test-host.sh # clipping and portable profile tests
./scripts/check-release.sh # ABI and redistribution checks
./scripts/device-suite.sh # on-device acceptance suite (run ON the tablet,
./scripts/device-suite.sh # Paper Pro on-device suite (run ON the tablet,
# detached — see the header comment)
```

Takeover sessions hold a kernel wakelock (`/sys/power/wake_lock`): autosleep
is active on this firmware and a mid-session suspend resumes into a second
xochitl instance fighting the app for the panel. `scripts/takeover.sh`
handles this; see `docs/device-test-2026-07-12.md` for the incident record.
On Paper Pro firmware exposing legacy kernel wakelocks, `scripts/takeover.sh`
holds one while xochitl is stopped. The RM2 firmware tested here does not
expose that interface.

Framebuffer selection and display behavior must also be tested on the target
hardware. If initialization reports multiple valid candidates, validate them
Expand All @@ -72,8 +89,16 @@ proprietary and is neither covered by that license nor redistributed here.

## C ABI policy

Use `quill_buffer()` to write pixels and one of these swap calls to push dirty
rectangles:
After `quill_init()` succeeds, `quill_get_surface()` returns the canonical
display view: vendor-owned pixels together with coherent geometry, stride,
format, model, capabilities, and pen orientation. The scalar
`quill_buffer()`/geometry getters remain for ABI compatibility. Pixel helpers
in `src/quill_surface.h` support both RGB32 and RGB16 surfaces.

Unknown panel geometry initializes as `QUILL_MODEL_UNKNOWN` with no optional
capabilities or pen orientation, keeping generic monochrome discovery usable
without enabling model-specific behavior. Write pixels, then use one of these
swap calls to push dirty rectangles:

```c
quill_swap_mono_fast(x, y, w, h); // Mono, mode 0, full=0: live ink
Expand All @@ -89,7 +114,7 @@ work: the vendor backend may promote `CompleteRefresh` to a whole-panel update
even when the supplied rectangle is small. `quill_swap_mono_fast()`,
`quill_swap_mono_quality()`, and `quill_swap_color()` are partial by contract.

Empirical Paper Pro findings:
Empirical Paper Pro findings (the RM2 is monochrome):

- `QUILL_CONTENT_COLOR + mode 3/4/5` renders color.
- `QUILL_CONTENT_COLOR + mode 1` renders grayscale/mono.
Expand Down
79 changes: 56 additions & 23 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
#!/bin/bash
# Cross-build quill against the ferrari SDK (OS 3.26 toolchain).
# Prereq: ~/rm-sdk-3.26 installed; libqsgepaper.so pulled from the device into ./vendor/.
# Cross-build quill against a matching reMarkable SDK.
# Usage: ./build.sh [ferrari|rm2]
set -euo pipefail
cd "$(dirname "$0")"

SDK=~/rm-sdk-3.26
ENV=$(ls $SDK/environment-setup-* | head -n1)
TARGET=${1:-${QUILL_TARGET:-ferrari}}
case "$TARGET" in
ferrari) DEFAULT_SDK="$HOME/rm-sdk-3.26"; EXPECTED_ARCH=aarch64 ;;
rm2) DEFAULT_SDK="$HOME/rm-sdk-3.27-rm2"; EXPECTED_ARCH=arm ;;
*) echo "unsupported target: $TARGET (expected ferrari or rm2)" >&2; exit 2 ;;
esac

SDK=${QUILL_SDK:-$DEFAULT_SDK}
ENV=
for candidate in "$SDK"/environment-setup-*; do
if [ -f "$candidate" ]; then
ENV=$candidate
break
fi
done
if [ -z "$ENV" ]; then
echo "no SDK environment found under $SDK" >&2
exit 2
fi
# The SDK env script sets CC/CXX with target flags and $SDKTARGETSYSROOT.
# It refuses to load when LD_LIBRARY_PATH is set.
unset LD_LIBRARY_PATH
source "$ENV"
if [ "${OECORE_TARGET_ARCH:-}" != "$EXPECTED_ARCH" ]; then
echo "SDK target architecture '${OECORE_TARGET_ARCH:-unknown}' does not match $TARGET ($EXPECTED_ARCH)" >&2
exit 2
fi

mkdir -p build vendor
if [ ! -f vendor/libqsgepaper.so ]; then
echo "pulling libqsgepaper.so from device..."
scp -O rm:/usr/lib/plugins/scenegraph/libqsgepaper.so vendor/
SDK_VENDOR_DIR="$SDKTARGETSYSROOT/usr/lib/plugins/scenegraph"
if [ -f "$SDK_VENDOR_DIR/libqsgepaper.so" ]; then
VENDOR_DIR=$SDK_VENDOR_DIR
elif [ -f vendor/libqsgepaper.so ]; then
VENDOR_DIR=vendor
else
echo "libqsgepaper.so was not found in the target SDK or vendor/" >&2
exit 2
fi

QTINC="$SDKTARGETSYSROOT/usr/include"
Expand All @@ -24,61 +50,68 @@ QTINC="$SDKTARGETSYSROOT/usr/include"
$CXX -fPIC -shared -O2 -std=c++17 \
-I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \
src/vendor_probe.cpp src/quill_c.cpp \
-L vendor -lqsgepaper -lQt6Gui -lQt6Core -ldl \
-L "$VENDOR_DIR" -lqsgepaper -lQt6Gui -lQt6Core -ldl \
-o build/libquill.so

# Non-destructive on-device initialization/ABI probe.
$CC -O2 -I src tests/device_init_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/device_init_probe

# Small self-restoring on-device swap/ABI probe.
$CC -O2 -I src tests/device_swap_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/device_swap_probe

# Automated on-device clipping, mode, stress, and refresh acceptance probe.
$CC -O2 -I src tests/device_acceptance_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/device_acceptance_probe

# Human-verified visual acceptance probe (orientation, channel order, modes).
$CC -O2 -I src tests/device_visual_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/device_visual_probe

# Signal-termination lifecycle probe without input or display modifications.
$CC -O2 -I src tests/device_term_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/device_term_probe

# The demos below still use Paper Pro-specific input and pixel assumptions.
# Keep the RM2 target limited to the portable adapter and device probes.
if [ "$TARGET" = rm2 ]; then
echo "built: build/libquill.so build/device_init_probe build/device_swap_probe build/device_acceptance_probe build/device_visual_probe build/device_term_probe"
exit 0
fi

# scribble: the C1 latency demo.
$CC -O2 src/scribble.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/scribble

# map_demo: static full-screen map + tiny partial-update footsteps.
$CC -O2 src/map_demo.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/map_demo
Expand All @@ -88,39 +121,39 @@ $CXX -O2 \
-I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \
src/image_demo.cpp \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/image_demo

# color_probe: experimental EPContentType::Color / ACEP path probe.
$CC -O2 src/color_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/color_probe

# color_mode_compare: same pattern, side-by-side, refreshed with different modes.
$CC -O2 src/color_mode_compare.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/color_mode_compare

# color_partial_probe: small dirty-rect color additions/erasures/churn.
$CC -O2 src/color_partial_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/color_partial_probe

# color_blend_probe: software alpha blending, color mixing, and stacking.
$CC -O2 src/color_blend_probe.c \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/color_blend_probe
Expand All @@ -130,7 +163,7 @@ $CXX -O2 \
-I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \
src/color_image_demo.cpp \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/color_image_demo
Expand All @@ -140,7 +173,7 @@ $CXX -O2 \
-I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \
src/image_anim_demo.cpp \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/image_anim_demo
Expand All @@ -150,7 +183,7 @@ $CXX -O2 \
-I "$QTINC" -I "$QTINC/QtCore" -I "$QTINC/QtGui" \
src/gif_demo.cpp \
-L build -lquill \
-L vendor -lqsgepaper \
-L "$VENDOR_DIR" -lqsgepaper \
-lQt6Gui -lQt6Core -lstdc++ \
-Wl,-rpath,/home/root/quill \
-o build/gif_demo
Expand Down
30 changes: 26 additions & 4 deletions scripts/check-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,43 @@ set -eu
cd "$(dirname "$0")/.."

fail() { echo "release check: $*" >&2; exit 1; }
warn() { echo "release check: warning: $*" >&2; }

strict=${QUILL_RELEASE_STRICT:-0}
case "$strict" in
0|1) ;;
*) fail "QUILL_RELEASE_STRICT must be 0 or 1" ;;
esac

[ -f LICENSE ] || fail "LICENSE is missing"
[ ! -e src/epfb.cpp ] || fail "prohibited historical implementation is present"
[ ! -e src/epframebuffer.h ] || fail "prohibited historical header is present"
if git ls-files 'vendor/*.so' | grep -q .; then
if git ls-files | grep -Eq '(^|/)libqsgepaper\.so([.]|$)'; then
fail "a proprietary vendor library is tracked"
fi

if [ "$strict" -eq 1 ] &&
[ -n "$(git status --porcelain --untracked-files=all)" ]; then
fail "strict release checks require a clean worktree"
fi

if [ -f build/libquill.so ]; then
required='quill_init quill_width quill_height quill_stride quill_format quill_buffer quill_swap_ex quill_swap quill_swap_mono_fast quill_swap_mono_quality quill_swap_color quill_swap_color_full quill_process_events'
required='quill_init quill_width quill_height quill_stride quill_format quill_buffer quill_get_surface quill_supports_color quill_swap_ex quill_swap quill_swap_mono_fast quill_swap_mono_quality quill_swap_color quill_swap_color_full quill_process_events'
exports=$(nm -D --defined-only build/libquill.so)
for symbol in $required; do
echo "$exports" | grep -Eq "[[:space:]]${symbol}$" || fail "missing export $symbol"
printf '%s\n' "$exports" | grep -Eq "[[:space:]]${symbol}$" || \
fail "missing export $symbol"
done
for constructor in C1 C2; do
printf '%s\n' "$exports" | grep -Eq \
"_ZN6QImage${constructor}EPhiiiNS_6FormatEPFvPvES2_$|_ZN6QImage${constructor}EPhiixNS_6FormatEPFvPvES2_$" || \
fail "missing QImage $constructor constructor interposition export"
done
elif [ "$strict" -eq 1 ]; then
fail "build/libquill.so is required for a strict release check"
else
warn "build/libquill.so is absent; binary ABI checks were skipped"
fi

git diff --check
git diff --check HEAD --
echo "release checks passed"
Loading