Case study · October 2026

Putting a robot's touchscreen UI on the GPU

The cleaning robot I support at work had a laggy touchscreen. I got shell access, found out why, proved a full rewrite could hit 60 fps, and then chose the fix the team could actually ship.

Ozair Khan · ozairk11@gmail.com · github.com/ozdotdotdot

17.7 → 42–45 fps
Electron on the GPU vs. software rendering (test page), about 2.4x
51–60 fps
Rust/Slint prototype on the same GPU, capped by the 60 Hz panel
80 min
soak with flat memory and zero GPU, kernel or JS errors
Same robot, same gestures: the stock software-rendered UI, then the GPU-rendered one.

01The problem

The robot's touchscreen runs an Electron + React app on an NXP i.MX8 board (aarch64, Vivante GC7000 GPU, Weston compositor), inside a Docker container. It felt sluggish: taps lagged and animations stuttered. The people using that screen are hotel housekeeping staff, every day, so it is a large part of how customers experience the product.

I'm on the customer success team, not engineering. I asked if I could take a look, and got SSH access to a dev robot.

02Measure before touching anything

The board has no FPS counter for Electron, so I wrote one. Every GPU job raises an interrupt, so a small read-only script samples /proc/interrupts for the GPU driver's lines every 2 ms and turns the timestamps into a frame-cadence estimate. Combined with per-thread CPU from top -H, that gave a baseline:

03The ambitious path: prove what the hardware can do

Before fixing the old stack, I wanted to know the ceiling. I rebuilt the main screens in Rust with Slint, a GPU-native UI toolkit, matching the existing design pixel for pixel (0.4–3% pixel difference on most screens against the component library).

Frames per second, estimated from GPU interrupt timing. The loads differ (manual taps vs. a scripted loop), so treat this as indicative, not a controlled A/B.

It was butter smooth. It also would have meant rewriting a whole production UI, which no team takes on lightly.

04The pragmatic path: fix the app they already have

The obvious move, turning hardware acceleration back on, doesn't help by itself. I found three independent blockers, and each one alone keeps the GPU off:

  1. The app turns the GPU off itself. Its entry point disables GPU acceleration unconditionally, so no launch flag can override it.
  2. Wrong display path. The UI ran as an X11 app through Xwayland, and the Vivante driver only accelerates Wayland (and GBM), not X11.
  3. The container can't reach the GPU. The UI's user isn't in the video group that owns the GPU device, and the container only ships generic Mesa libraries, not the vendor driver.
ChangeWhy
Neutralize the two GPU kill-switch callsNothing else matters until they're gone
Mount the Wayland socket, run with --ozone-platform=waylandThe driver only speaks Wayland
Map /dev/galcore, add the video group, load the vendor EGL/GLES libraries firstThe container's Mesa can't drive this GPU
--use-gl=egl --in-process-gpu --ignore-gpu-blocklist --enable-gpu-rasterizationA separate GPU process has no Wayland display, so the GPU work has to run in-process
Turn off MSAA and stencil buffers for rasterizationWithout it, SVG icons silently vanish on this driver

Traps worth knowing: mapping /dev/dri makes Chromium pick GBM buffers and crash; the missing icons only showed up on certain screens; and the app crashes at launch without a writable config directory.

I tested everything in a separate, throwaway, network-isolated container, and production containers were never modified. The result runs the unmodified UI code on the GPU, side by side with the normal UI on the live robot.

05Results

CheckResult
Frame rate (GPU test page)17.7 → 42–45 fps
Real UI on the live robotStart, pause, stop and the run summary all worked with no errors; noticeably smoother
80-minute idle soakMemory flat throughout; no JS, GPU or kernel errors; no crashes; temperature peaked at 84 °C and settled to 72 °C
CPU and memory at idle~6–7% CPU and ~340 MB vs. ~8.5% and 430–510 MB stock (a rough read, not a controlled test)

06What I'd still verify before rolling it out

07What I took from it

The rewrite was the more fun build, and it proved the hardware could hit 60 fps. But the job was to make the robot better for the people using it, and a handful of mounts and flags gets most of that win at a fraction of the risk. Building the ambitious version first is what made me confident the small fix was the right one.