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
20 changes: 10 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ version = "1.5.0"
authors = ["Rien Maertens <rien.maertens@posteo.be>"]
edition = "2024"

[features]
default = []
# Pen cursor overlay — requires libremarkable (armhf/rM2 only, not aarch64/Tatsu)
cursor = ["libremarkable"]

[dependencies]
anyhow = "1.0"
lz-fear = "0.2"
clap = { version = "4.5", features = [ "derive" ] }
libremarkable = { version = "0.7", features = [ "input" ], default-features = false }
libremarkable = { version = "0.7", features = [ "input" ], default-features = false, optional = true }
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ reMarkable screen sharing over SSH.

[![rm1](https://img.shields.io/badge/rM1-supported-green)](https://remarkable.com/store/remarkable)
[![rm2](https://img.shields.io/badge/rM2-supported-green)](https://remarkable.com/store/remarkable-2)
[![rmpaperpure](https://img.shields.io/badge/rM_Paper_Pure-supported-green)](docs/tatsu.md)

![A demo of reStream](extra/demo.gif)

Expand Down Expand Up @@ -90,6 +91,8 @@ $ scp restream.arm.static root@10.11.99.1:/home/root/restream
$ ssh root@10.11.99.1 'chmod +x /home/root/restream'
```

> **Note for reMarkable Paper Pure users:** the Paper Pure uses an aarch64 processor and requires a separate binary. See [docs/tatsu.md](docs/tatsu.md) for build instructions.

## Usage

1. Connect your reMarkable with the USB cable.
Expand Down Expand Up @@ -179,6 +182,8 @@ If you want to play with the `restream` code, you will have to [install Rust](ht

There are three ways of building the required restream binary for streaming the reMarkable framebuffer. For these approaches, the generated restream binary will be located under `target/armv7-unknown-linux-gnueabihf/release/restream`.

> **reMarkable Paper Pure:** requires an aarch64 binary instead. See [docs/tatsu.md](docs/tatsu.md).

- **Using nix flakes**
With [Nix](https://nixos.org/guides/install-nix.html) installed you can
create the development environment with `nix-shell` or (when using
Expand Down
112 changes: 112 additions & 0 deletions docs/tatsu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# reMarkable Paper Pure (Tatsu) Support

## Device Overview

| Property | Value |
|---|---|
| Product name | reMarkable Paper Pure |
| Internal codename | Tatsu |
| SoC | NXP i.MX 93 (aarch64, ARM Cortex-A55) |
| Kernel machine string | `reMarkable Tatsu` |
| OS version location | `/etc/os-release` → `IMG_VERSION` |
| Confirmed working on | firmware 3.28.0.172 |

## Display Hardware

| Property | Value |
|---|---|
| Panel | E Ink Carta™ 1300 |
| Physical resolution | 1872 × 1404 (landscape) |
| DRM driver | `eink-carta-panel` |
| DRM mode | 260×1408 (waveform output, not pixels) |

The display is driven via DRM/KMS through `/dev/dri/card0`. The DRM mode
(260×1408) is the packed E Ink waveform output size, not the pixel resolution.

## How It Works

xochitl renders the UI into a **BGRA color buffer** in its own heap memory. This
is the same buffer its built-in screen share feature reads from. reStream reads
directly from this buffer via `/proc/<pid>/mem`.

### Finding the buffer

The buffer is located by scanning `/proc/<xochitl_pid>/maps` for the first
anonymous `rw-p` mapping of 10–16 MB that appears immediately after the last
`/dev/dri/card0` entry. This is reliable across restarts as long as the firmware
version does not change.

### Buffer layout

The buffer stores the display content in **portrait orientation** (1404×1872),
regardless of how the device is held. The landscape content is rotated 90° CCW
into the portrait frame by xochitl.

| Property | Value |
|---|---|
| Pixel format | BGRA (4 bytes/pixel) |
| Frame dimensions | 1404 × 1872 pixels |
| Stride | 1404 × 4 = 5616 bytes/row |
| Frame size | 1404 × 1872 × 4 = 10,513,152 bytes |

### Skip offset

The buffer contains a transparent (alpha=0) region before the visible content.
The skip offset is deliberately **non-row-aligned** so that reading wraps a fixed
number of columns into the next row, achieving a circular horizontal shift that
aligns the left edge of the canvas with the start of the output frame. This is
the same technique used for rM2.

```
skip = 468 blank rows × 5616 bytes/row + 340 transparent columns × 4 bytes
= 2,628,288 + 1,360
= 2,629,648 bytes
```

The 340-column offset was confirmed by dumping the first row of content and
finding the first opaque pixel (`00 00 00 ff`) at byte offset 1360.

## reStream.sh Parameters

```sh
"reMarkable Tatsu")
width=1404
height=1872
bytes_per_pixel=4
fb_file=":mem:"
pixel_format="bgra"
skip_offset=2629648
dark_mode=false
if $landscape; then
extra_video_filters="transpose=2"
fi
landscape=false
;;
```

`transpose=2` (90° CCW) rotates the portrait buffer back to landscape.
Use `-p` to stream in portrait orientation without any rotation.

Note: `transpose=1` (the rM2 default via `landscape=true`) produces an
upside-down result on Tatsu — use `transpose=2` instead.

## Building

The binary must be built for `aarch64` (the Tatsu has no 32-bit compatibility):

```sh
cross build --release --target=aarch64-unknown-linux-musl
scp target/aarch64-unknown-linux-musl/release/restream root@10.11.99.1:~/restream
```

The `--features cursor` build flag is **not supported** on Tatsu — it requires
`libremarkable` which is armhf-only.

## DRM PRIME (investigated, not viable)

DRM PRIME is a mechanism for sharing GPU memory buffers between processes via
file descriptors. On Tatsu, xochitl holds 17 GEM buffer handles for the E Ink
waveform pipeline. We investigated exporting these via `pidfd_getfd()` +
`DRM_IOCTL_PRIME_HANDLE_TO_FD`, but all 17 buffers contain E Ink waveform data
(electrical pulse sequences for the panel), not pixel data. The pixel data lives
in an anonymous heap mapping which PRIME cannot export.
22 changes: 21 additions & 1 deletion reStream.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ ssh_cmd() {
}

is_current_rm_firmware_version_ge() {
current_rm_firmware_version=$(ssh_cmd "grep 'REMARKABLE_RELEASE_VERSION=' /usr/share/remarkable/update.conf | cut -d '=' -f2")
# Try update.conf (rM1/rM2), fall back to /etc/os-release IMG_VERSION (Tatsu/newer)
current_rm_firmware_version=$(ssh_cmd "grep 'REMARKABLE_RELEASE_VERSION=' /usr/share/remarkable/update.conf 2>/dev/null | cut -d '=' -f2")
if [ -z "$current_rm_firmware_version" ]; then
current_rm_firmware_version=$(ssh_cmd "grep 'IMG_VERSION=' /etc/os-release 2>/dev/null | cut -d '=' -f2 | tr -d '\"'")
fi
test "$(printf '%s\n' "$current_rm_firmware_version" "$1" | sort -rV | head -n 1)" = "$current_rm_firmware_version"
}

Expand Down Expand Up @@ -169,6 +173,21 @@ case "$rm_version" in

skip_offset=8
;;
"reMarkable Tatsu")
width=1404
height=1872
bytes_per_pixel=4
fb_file=":mem:"
pixel_format="bgra"
skip_offset=2629648
dark_mode=false
# The buffer is portrait (1404×1872). For landscape view (default),
# rotate 90° CCW. For portrait (-p), show as-is.
if $landscape; then
extra_video_filters="transpose=2"
fi
landscape=false
;;
"reMarkable 2.0")
if ssh_cmd "[ -f /dev/shm/swtfb.01 ]"; then
width=1404
Expand Down Expand Up @@ -313,6 +332,7 @@ set -e # stop if an error occurs

restream_options="-h $height -w $width -b $bytes_per_pixel -f $fb_file -s $skip_offset"


if "$cursor"; then
restream_options="$restream_options -c"
fi
Expand Down
Loading