What is actually underneath
Roblox ships no Linux client. Cordial does not emulate one — it replaces the platform the Android engine expects to find, and lets the engine itself run unmodified.
Bionic linker
AOSP's dynamic linker, ported to run on glibc Linux. It loads libroblox.so the way Android would, resolving the same symbols in the same order.
ABI shim
Bionic and glibc disagree in ways that corrupt memory silently. struct addrinfo has its last two pointers transposed; sem_t is 16 bytes against 32.
libjnivm
A JNI virtual machine in place of ART. The engine calls into Java expecting Android's framework; Cordial answers those calls without an Android runtime.
Framework layer
The Android surface the client actually uses — GameActivity, input, OpenSL ES, the settings and flag services — implemented against Wayland, PipeWire and D-Bus.
Status, including what does not work
This table is the most useful thing on the page, and the rows marked no are why. A project that only lists its wins is telling you nothing.
| Area | State | Detail |
|---|---|---|
| Sign in | yes | Session persists across restarts, stored in the desktop keyring. |
| Play a game | yes | Load in, move, turn the camera. Reported smooth on integrated graphics. |
| Keyboard | yes | The engine wants evdev codes, not Android keycodes. Four theories died before that surfaced. |
| Pointer capture | partial | Locked pointer and relative motion are wired; that a granted lock turns the camera is INFERRED. |
| Audio | partial | Sound leaves the OpenSL ES bridge into PipeWire, measured. Never yet verified inside an experience. |
| Profiles | yes | Multiple accounts, one flock per profile, several instances at once. |
| Plugins | yes | Separate processes, named capabilities, default-deny. Two ship in-tree. |
| Text fields | no | Characters do not paint until the field loses focus. Needs an EditText-equivalent overlay. |
| Fullscreen | no | Clips and letterboxes until you switch workspaces and back. |
| Web views | no | Unimplemented, which is why a good deal of Roblox's own UI does nothing. |
A plugin never gets the socket
Discord Rich Presence ships in the tree, and it is the worked example for how every host resource is meant to reach a plugin — which is to say, not by being handed over.
plugins/discord-presence/ is first-party in the sense that it comes with
Cordial and in no other: an ordinary manifest, ordinary grants, the same isolation as
anything you write. It requests three capabilities — lifecycle.read,
presence.set, log — and holds nothing else. It publishes a
presence when the client launches and again when it is ready, and clears it on shutdown.
What it never learns is where Discord’s IPC socket is. The plugin sends a presence payload; Cordial searches the socket paths, performs Discord’s handshake and writes the frames. The payload is a closed structure that refuses any field Discord does not define, so nothing a plugin invents crosses the wire, and it cannot read Discord’s state or send anything else down the connection. A Flatpak permission is app-wide and permanent; a capability is per-plugin and revocable. If installing a plugin could add a permission, uninstalling it could not take one away — which is the whole argument for brokering the effect rather than granting the channel.
Installing it is a copy into the plugin directory. Approving it is a file, per profile, because default deny means the grants file is the only thing that grants:
# ~/.local/share/cordial/profiles/<profile>/plugin-grants.json
{ "discord-presence": ["lifecycle.read", "presence.set", "log"] }
None of it reaches Discord in a real session yet. The broker, the payload validation and Discord’s wire framing are covered end to end by a test that spawns the shipped plugin as a real Deno process and watches the frames land on a stand-in socket.
But the plugin host the client runs serves settings, flags and logging, and
answers everything else with not implemented yet — and nothing outside
that test pushes a lifecycle event. So a granted plugin starts, asks to subscribe, and is
told the method is not implemented. That is read from the source rather than measured in
a session, and joining the two hosts up is the first thing to do if you want this
working.
Things this project got wrong, and corrected
Commits exist here whose only purpose is to withdraw an earlier claim. They are the good ones. If you are evaluating whether this codebase can be trusted, read these rather than the feature list.
Frame rate
Presents per second is the frame rate, and it collapses after thirteen seconds.
It was an idle throttle. Presents drop to exactly 1.0/s when nothing is happening. Every figure recorded before this was that curve integrated, and several had been quoted as evidence.
Keyboard
nativePassKeyEvent takes Android keycodes; the mapping table must be wrong.
It takes evdev codes. Four theories failed because each assumed a wrong number when the vocabulary was wrong. One logcat capture answered it in a minute.
ABI
pthread_cond_t is 32 bytes in bionic.
It is 48. 32 is pthread_barrier_t. The wrong number sat in the record for months.
Distribution
Sober fetches the same build from the same place, in the open.
It does not. Sober routes through Google Play. Roblox publishes no Android artefact at all — three endpoints checked, absent from every one. That sentence is struck through in the ADR rather than deleted.
Diagnosis
The system is thrashing on swap; that is the performance problem.
A cumulative counter read as a rate. Sampled live, swap-out was zero. The reported slowdown was unrelated software on the same desktop.
Running it
Cordial ships no Roblox code and never will, so you supply the build. There is no download button, because Roblox publishes no Android file to download — it distributes through Google Play and the Amazon Appstore.
# Add the remote and install. This is the way to install Cordial.
flatpak remote-add --if-not-exists cordial \
https://luohoa97.github.io/cordial/cordial.flatpakrepo
flatpak install cordial io.github.luohoa97.Cordial
Then launch it from your application list, or flatpak run
io.github.luohoa97.Cordial. flatpak update picks up new builds.
Cordial is not on Flathub, and on current policy it cannot be.
Flathub does not allow applications containing AI-generated or AI-assisted code,
documentation or content, and Cordial contains a great deal of both — its git
history records that in Co-Authored-By trailers rather than hiding it.
So this remote is the distribution channel, not a stopgap until a better one arrives.
The remote is unsigned. flatpak install checks that what
it downloaded matches the repository’s own checksums, and nothing beyond that —
it does not prove who built it. Anyone who can write to this GitHub Pages site can
serve a different package under the same name. That is a weaker guarantee than
Flathub’s, and you should know which one you are taking.
Building from source is for changing Cordial rather than for running it:
# Clang is required; AOSP bionic does not build with GCC. cargo build --release cargo test --workspace # Point it at an APK you already have. cargo run --release --bin cordial-run -- \ --lib-dir /path/to/lib/x86_64 --apk /path/to/base.apk \ --host-libc --game-activity
This project needs a maintainer
Its author has stepped back and intends to return to hand it over rather than to resume work on it. That is stated here for the same reason it is stated at the top of the README: finding out after investing a weekend would be worse.
It is not abandoned mid-collapse. It works, it is documented, and
docs/HANDOVER.md
was written for whoever takes it on — every open thread with what is genuinely known about
it, which claims are INFERRED and why, and the measurement traps that have
already cost people afternoons.
If you would rather start small, the good first issues are real, and none of them needs a Roblox account — worth saying, because a good deal of the remaining work does.
And if you fork it: please open a pull request as well. Not instead. A change that lives only in a fork becomes archaeology; a change that exists as a pull request can be merged in ten minutes by whoever picks this up.