Building a Compositor for the reMarkable Tablet
Introduction
Something that was bugging me while developing software for the reMarkable has been all the hacky workarounds we've had to do in order to handle displaying applications on top of each other. Generally, you have to pause the previous application with a SIGSTOP, and then resume it when you are done with a SIGCONT. But before you can resume it, you'll need to flood the input buffer to avoid having the application replay any input you've done while it was paused. You also need to manage returning the framebuffer to the state it was when the application was paused. This causes headaches when two applications don't play nice with how they've implemented this, as there is no standard that everybody uses. Mostly just copied and tweaked code between all the different projects.
A solution for this is a display server that both composites the buffers for each of the applications onto the display, but also serves as an input multiplexer, and only sends the events to the application that needs it. We already have solutions for this in the industry, the aging X Window System, and the newer Wayland protocol. So the obvious question here is, why didn't I just cross compile one of those to the device?
I did think about using Wayland, but after looking into what it would take to cross compile, not to mention how fragmented wayland is, I decided it would be less fun that just writing something purpose-built for the device. I also wouldn't have to deal with sorting out how to get Wayland to play nice with rm2fb, as it wasn't really something I was interested in fighting with.
Proof of concept
I took an initial stab at writing the compositor in July 2023 and got it into a mostly working state by August. I started daily driving the new compositor, but stopped working on it until December. At that point in time I was already not happy with the current state of the codebase and some architectural decisions I had made. By January I decided to start over, using what I had learned to fast-track the new development.
Actual implementation
https://github.com/Eeems-Org/oxide/pull/348
This time I made the compositor it's own application, instead of building it into the oxide system service, this had three main benefits:
- If the system service crashes and restarts, the compositor can stay running.
- The compositor can be used without oxide.
- Iterating was faster as I didn't have to wait for the tarnish system service to startup every time I needed to test a change.
I decided that this time, while I would use DBus like the oxide system service, and thus the previous implementation did for the main entrypoint into the API, I would implement a socketpair based binary protocol private to each connection to avoid the latency that DBus added for the timing critical compositing and input muxing.
This new implementation performed much better, and I worked on it on and off throughout 2024, finally deciding to merge it in October 2024 and work on remaining items as individual pull requests.
C-abi
In march 2025, after some discussions in the reMarkable community discord I decided to write a library for the compositor protocol that could be used from C, instead of the existing C++ based library. This would allow any language to use this library using a C foreign function interface, which is the lowest common denominator between most languages. I also updated my C+ based library to use this C library as the base for many parts of it.
Qt6 and aarch64 device support
https://github.com/Eeems-Org/oxide/pull/394
In February of 2025, asivery had started the work to port oxide and the compositor to qt6, the latest version of libqsgepaper. This would move oxide from being stuck on the older versions of the OS that toltec supported to the latest stable releases. This work stalled though, and I eventually picked it up in earnest in in June 2026.
https://github.com/Eeems-Org/oxide/pull/418
I finished porting all of oxide, including the new compositor to Qt6, and added support for the newer aarch64 reMarkable devices. I then started my initial 3.0 pre-releases later in June.
Finally on July 8th 2026 I released Oxide 3.0 and the new compositor. It had been a long nearly 2 year development cycle, but it was finally released.
The protocol
The protocol itself has 3 parts. The first is a DBus interface for applications to initiate communication with the display server. This has high level methods that are not performance critical, as well as methods that are only meant for applications with system permissions to use.
After connecting to the DBus service, an application can open the connection socket, which is where the rest of the API lives.
Input is handled by a shared ringbuffer instance that can be opened from the DBus service
All of this is wrapped in the libblight_protocol library, making it so that you don't have to implement the wire protocols at all.
The wire protocol
The wire protocol features asynchronous responses and variable sized packets. Since this socket is meant to just be local connection, we aren't concerned with packets being dropped, or only partially transmitted. Order of messages, and their contents is guaranteed. For performance reasons, we can't just send a message and then wait for the response directly. Thus the protocol needed to handle asynchronous responses. This allows fire and forget requests, and then you handle the response when it comes back later.
Each packet, or message as it's named in the code, contains a header, and then optionally variable length data. The header contains the type of message, the unique identifier for an asynchronous response (an ack), and the size of the data.
There are 11 message types currently defined for the wire protocol: Ack, Ping, Repaint, Move, Info, Delete, List, Raise, Lower, Wait, and Focus.
Ack
Whenever a message is sent to the compositor, it will send back an ack message containing the same unique identifier when it is done. These messages may also contain data, depending on what the initial request was.
Ping
When a ping message is sent by either the compositor or the client, the other side is expected to send an ack immediately. The compositor uses this to determine if a client has stopped responding.
Repaint
When the client wants to trigger a repaint of a surface, it will send a repaint message with data specifying what and how to repaint it. The compositor will not send an ack message.
Move
When the client wants to ask the display server to move a surface to a different location on the screen, it sends a move message with data specifying where to move it to. The compositor will not send an ack message.
Info
When the client wants to get the information on a specific surface, it will send an info message where the data is the surface identifier. The compositor will reply with an ack message that contains the surface information.
Delete
When the client wants to remove a surface, it will send a delete message where the data is the surface identifier. The compositor will reply with an ack message when the surface has been removed.
List
When the client wants to know what surfaces are available, it will send a list request. The compositor will reply with an ack message that contains the count of surfaces, and then an array of the surface identifiers.
Raise
When the client wants to raise a surface to the top of the stack, it will send a raise request where the data is the surface identifier. The compositor will not send an ack message.
Lower
When the client wants to lower a surface to the bottom of the stack and hide it, it will send a lower request where the data is the surface identifier. The compositor will not send an ack message.
Wait
When the client wants to wait for a repaint to finish, it will send a wait request with the repaint identifier. The compositor will send an empty ack message when the repaint is completed.
Focus
When the client wants to request focus, an empty focus message is sent. The compositor will not send an ack message.
The input protocol
The input buffer is a ringbuffer that is shared between the compositor and the client. It can hold 64 input_event structs, keeps track of if the client hasn't read events fast enough to keep up with the number of events being produced, and uses futex on top of std::atomic<uint32_t> structures to allow waiting for new events without wasting CPU cycles.
Cool projects using the compositor
Some final thoughts
This project has given me something to focus on during a very difficult time in my life. I learned a lot while working on it, and hopefully I've put something out that others will want to hack around with.
I work on these kinds of projects as a hobby, and only for myself. If others want to use it, that's a nice bonus, but that's not the goal. What I got out of the project was an opportunity to do something difficult and learn, as well as build the interface I wanted to use on my reMarkable tablets. It also gave me a nice distraction from what was happening in my life. I'm glad I spent the time I did on this, and I'm quite proud of the work that I've produced. Even if there are still 48 open bugs and enhancements on the oxide repository 😆