Skip to main content
0.13.0’s headline was that built-in plugins were never started. They are now, and it turned out to be the top layer of the same fault: no packaging format Cordial ships installs or bundles Deno, and plugins are Deno programs. Not the deb, the rpm, either AUR package, the AppImage or the Flatpak. So plugins still could not run anywhere except a machine that happened to have an interpreter already — which is what an Arch user reported, and they were right. Twenty-one commits. Also here: the AppImage’s web view, which never opened at all; a flash of the GTK background when / opens chat; a consent prompt that asked again every time Settings was opened; and a FastFlags page with colour and live errors.

Plugins had no interpreter, anywhere

deno is in Arch’s extra, and in neither Fedora (dnf5 list deno on 44 returns nothing) nor Debian (its own source index answers "exact": null). It went unnoticed here because this development machine has Deno from Homebrew. The Flatpak decides the shape of the fix. Inside one, PATH is the runtime’s, and Cordial deliberately takes no route to the host — flatpak-spawn --host needs a D-Bus name ADR-018 refuses, because it is the same name that runs an arbitrary command on the host. A Flatpak user therefore cannot install an interpreter by any means available to them. Cordial has to provide one or plugins are permanently dead there. So Settings, Plugins, grows a row only when there is no interpreter, with a Download button. It fetches a pinned Deno — version and SHA-256 both constants in the source, so nothing asks the network what the current release is — checks the hash over the stream before the file gets the name anything looks for, and extracts atomically, because the check that decides whether to download is “is there a file at this path” and a torn extract would be mistaken for an interpreter for ever. This is not a new class of trust: Cordial already downloads a 118 MB libroblox.so and loads it as native code in-process. Arch never fetches. Its package now depends on deno properly, and PATH is searched first. Both .SRCINFO files were stale and did not list it — that is the file the AUR actually reads for dependencies, so an AUR install would not have pulled Deno in even after the PKGBUILD was fixed. Corrected here, along with the missing bubblewrap optional dependency. The end-to-end download was run against Deno’s real release and the extracted binary reported the pinned version. What is not verified is the Settings row itself — this machine could not open a second Settings window while a client held the single-instance lock.

The AppImage could not open a web view

A user’s AppImage answered a web view with:
They then installed WebKitGTK on the host and it changed nothing, which is the detail that identifies the bug. The bundled libwebkitgtk-6.0.so.4 reaches five things through absolute paths fixed at its build time, and only Fedora and its derivatives put the helpers in /usr/libexec — Debian uses /usr/lib/x86_64-linux-gnu/webkitgtk-6.0, Arch /usr/lib/webkitgtk-6.0. So installing the host package leaves the baked-in path just as empty. WEBKIT_EXEC_PATH was already exported by AppRun and does nothing: it appears zero times in the library, against thirty-odd other WEBKIT_* names that do appear. A knob that looks like the fix and is inert is worse than no knob. The paths are therefore made to exist rather than redirected. AppRun re-execs under bwrap with an overlay over /usr/libexec — an overlay rather than a tmpfs so the host’s other contents survive — and binds the bundled helpers in at the name the library asks for. It probes first and falls through unwrapped, saying so, on a kernel or policy that refuses unprivileged overlayfs; CORDIAL_APPIMAGE_NO_WRAP=1 turns it off entirely. Verified with a control on a host standing in for one that never installed WebKitGTK: with the wrap, both helper processes ran from inside the image and a page reached load-changed FINISHED; same image, same host, CORDIAL_APPIMAGE_NO_WRAP=1, the reported error verbatim. What is not verified: the tested image’s binaries came from an installed rpm rather than a fresh build, the probe was a bare WebKitWebView rather than Cordial’s own sign-in view, and nothing but Fedora was tried. Two user reports, one cause each. The GTK background flashed when / opened chat, but only while a movement key was held. The restacking code already had the right idea — make the background transparent, repaint, then lower the subsurface — and relied on a repaint_now that was queue_draw(); pump();. The pump runs at most 32 non-blocking iterations, deliberately, because the engine’s pump must return. Idle, 32 iterations reach a frame easily. Holding a movement key fills the context with key repeats, the 32 are spent on events, and the frame clock never ticks inside them — so the restack went out under a parent that was still opaque. That is exactly why it needed a key held to reproduce, which is the part of the report that identified the mechanism. It now waits for after-paint on the frame clock, bounded by 40 ms, because the thing being waited for is a frame and frames are a duration. The consent prompt returned every time Settings was opened, even after the plugins were enabled. It was recorded in the response handler, which fires only if that dialog is answered — and every built-in plugin row presents its own prompt as the page renders, so several stack at once. Answer the top one, close Settings, and the rest are torn down with their parent without a response; nothing is written and all of them ask again. “Has this profile been asked” is now true the moment the question is on screen. ADR-003’s default deny is untouched: grants are still only written by the allow branch, so a dismissed prompt grants nothing and merely stops nagging. Neither is verified by observation. The flash is GTK’s own buffer showing through, which cordial_screenshot cannot see — it reads the engine’s swapchain — so confirming it needs a compositor capture at frame rate. The mechanisms are read from the code and the reports; the fixes follow from them. Worth a look before either is called done.

FastFlags: colour, and errors as you type

A comparison with Pigment, another Linux Roblox launcher, made two gaps obvious. Its editor is syntax-coloured; ours was a wall of monospace where every flag name looked like every value. GtkSourceView would have added a runtime dependency to five packaging files to colour one text box, so the document is scanned here instead and painted with tags — two palettes, repainted when the system theme flips, because a text tag holds a literal colour and does not follow a theme the way a CSS class would. Applying was previously the only way to find out whether a paste parsed, so the feedback for a misplaced comma was a toast after a save that did not happen. The status line now names the error as you type and Apply goes insensitive while the document does not parse. Nothing was ever written in that state anyway; the only change is whether pressing the button is how you find out. The actions moved, for the third time, into a row beneath the editor with the file path. The first two arrangements were each half right, and the lesson from the second was the wrong one: the problem was that nothing was grouped, not where the group sat. The scanner is tested; the colours are not. No nested compositor was available to photograph the rendering.

Discord presence

A game’s cover art, not Cordial’s icon. A game that set largeImage through BloxstrapRPC got Cordial’s logo, because the picture was thrown away on the way to the payload by a comment that contradicted the one a screen above it. And a presence for games that send no RPC at all, field for field the shape Bloxstrap uses, read from its source (MIT) rather than guessed: the experience name, by <creator>, the game’s icon as the large image, the Roblox badge as the small one, and the join time as the start. Every default goes in only where the game said nothing, so a game that speaks still wins. One correction, and it is the important one. The previous attempt at this shipped a URL that returns 404, and it was reported here as working because Discord accepted it — Discord rewrites any URL to its proxy form without fetching it, so acceptance measures nothing. The result on a real profile was a broken-image placeholder where Cordial’s own icon used to be, which is worse than the bug it replaced. The check that settles it is curl against the URL, and it takes one command. The working endpoint is thumbnails.roblox.com, and Cordial now resolves pictures through it, cached, off the log-watching thread. A plugin never handles a URL. Cordial issues an opaque key per picture and the payload echoes one back; an unresolved key renders as no image. That was the alternative to host-checking a URL and it is the stronger one: checking hosts still lets a URL cross the boundary and then argues about which ones are acceptable, whereas a key cannot be a link at all. The small image will not appear yet. Bloxstrap’s small image key is the literal string roblox, a Discord application asset rather than a URL, which renders only once an image of that name is uploaded to the application’s Rich Presence assets. Only the application’s owner can do that. Until then Discord draws no badge and nothing else changes. Games are now told this launcher speaks BloxstrapRPC. FFlagUserLaunchedWithBloxstrap is how an experience finds out whether the launcher under it implements the protocol, and a game that gates on it says nothing at all. Bloxstrap’s own RPC test place reports the answer on a board, and against Cordial the board said NO. It is not an engine flag — the name appears nowhere in libroblox.so; the launcher injects it and the game asks about it. A user’s own FastFlags still overrule it. And the log tells the truth now. The broker wrote the frame, read Discord’s reply and threw it away. Discord refuses an activity it will not store with an equally well-formed frame carrying evt: "ERROR", so a refused presence was returned to the plugin as ok. Every “presence.set came back: ok” in a log from 0.13.0 or earlier means “Discord answered”, not “Discord accepted”, and anything concluded from those lines needs re-reading with that in mind.

Launch stopped scanning 118 MB of engine

Profiled with perf before porting anything from another project, and the largest named cost in Cordial’s own code was ours alone: reading the engine’s version, so the client can tell the server which build it is, scanned the whole of libroblox.so byte by byte at every launch. It is now cached against the file’s length and mtime, so a launch reads a stamp instead.

The TaskScheduler crash: three more reports, and a diagnosis

Two more users hit Can't initialize the TaskScheduler before flags have been loaded — Linux Mint and Arch, on top of CachyOS twice. That is three distributions across both the AppImage and the Flatpak, so the distribution is not the variable and neither is the packaging format. The Mint log is the first with the whole startup in it, and it names something two sessions of inference could not: on that machine the engine build does not export nativeInitClientSettings under the name Cordial looks for, so the flags were never delivered at all and the engine crashed on exactly what it said. The two-physical-core theory that looked so promising was tested with taskset on the same binary and profile and did not reproduce. Cordial now says so when a native is missing instead of skipping it in silence, and the report names its own answer. That is a diagnostic, not a fix: the engine version there is 2.734.0.917 against 2.736.0.1408 here, and a JNI method renamed or moved between builds cannot be tested from this machine. If you hit this, the log now carries the evidence.

Measured and not changed

Three things were investigated at length and produced no code, which is worth saying so nobody reopens them from a title. The startup freeze holds nothing. Three frozen clients were caught under gdb on a signed-in profile. The engine thread is parked, not spinning — its poll count moves 199 in ten seconds at 1.6% of a core, which is the ceiling the idle backoff produces — and not one of 73 threads is on a mutex. It is waiting on an in-process pipe nobody writes to. That refutes two claims in the investigation notes, which have been corrected. No performance work was worth taking from mocktail. The lock traffic that a lock-free JNI scheme would have targeted is 4.7% of startup, and LBR call stacks put 177 of 179 caller frames inside libroblox.so — the engine locking against itself, with nothing on our side to make lock-free. Every other candidate died the same way, on measurement. All three clients were re-measured side by side, same method, same host, one at a time, three passes each — because the standing comparison was a month old on our side and older on everyone else’s:
Cordial uses half the memory of either, and barely moves — 10 MB of spread where mocktail swings 128. Peak startup CPU is no longer the outlier it was at 0.6.0 (342% against 142% and 153%). Three ways this is not like-for-like, all favouring Cordial and none corrected for: Cordial ran signed out on a fresh profile while the controls ran their existing signed-in installs; Cordial was one process against twelve and thirteen; and Cordial was a local build against two Flatpaks.

What is still broken

Everything 0.13.0 listed, unchanged: the startup freeze when signed in — better characterised now, not fixed — the black canvas inside an experience, text in a box disappearing when you click off it, the editor lagging a text box that Roblox is animating, and the web-view dialog’s clicks and cursor. The TaskScheduler crash above is not fixed, only diagnosed and instrumented. gamepadType’s ordinals are still unestablished. Settling it needs a controller and a game. Three of this release’s fixes have not been seen working, each for a stated reason: the / flash and the consent prompt (no compositor capture, no second Settings window), the FastFlags colouring (no nested compositor), and the plugin-download row in Settings. The mechanisms are established and the components underneath them are tested. That is not the same as having watched them work, and this note will be corrected rather than left wrong if any of them turns out not to.