Qt + EGLFS Dashboard
Time estimate: ~75 minutes (including host development) Prerequisites: DRM/KMS Test Pattern, Single-App Fullscreen UI
Learning Objectives
By the end of this tutorial you will be able to:
- Build and run a Qt6 Quick application on EGLFS (no X11, no Wayland)
- Use QML property bindings to connect C++ sensor data to UI elements
- Create custom gauge components using QML Canvas
- Compare Qt and SDL2 dashboards: binary size, RAM, startup time, development effort
Qt EGLFS: Declarative UI on Bare Metal
EGLFS (EGL Full Screen) is Qt's platform plugin for embedded systems without a compositor. It renders directly to the display via EGL and OpenGL ES through DRM/KMS — the same kernel path SDL2 uses with kmsdrm. The key difference is the programming model: Qt uses a declarative approach (QML) where you describe what the UI looks like and bind values to C++ data sources; the scene graph handles dirty-region tracking and redraws only what changed. SDL2 uses an imperative approach where you explicitly draw every primitive each frame. Qt brings richer built-in features (text rendering, touch gesture recognition, animations, layout managers) at the cost of a larger runtime (~40 MB RAM vs ~5 MB for SDL2) and slower startup (~2 s vs <0.5 s). Neither is universally better — the right choice depends on UI complexity, RAM budget, and development timeline.
See also: Graphics Stack reference | Qt Quick for Embedded Linux
Overview
The SDL2 Dashboard built the same three-panel display using SDL2 primitives — manually drawing arcs, rectangles, and text. This tutorial builds the same dashboard using Qt6 Quick and QML, rendering on EGLFS — Qt's platform plugin that renders directly to the GPU via EGL/OpenGL ES, bypassing X11 and Wayland entirely.
The key difference: in Qt, you declare what the UI looks like and bind values to data sources. When data changes in C++, QML automatically redraws the affected elements.
main.cpp (C++):
SensorBackend (QObject)
├── QTimer @ 10 Hz → reads sysfs, /dev/bmi160, /proc/stat
└── Q_PROPERTY: temperature, roll, pitch, cpuPercent
│
│ changed signals (automatic)
▼
main.qml (QML):
Window { FullScreen, EGLFS }
RowLayout {
GaugeArc { value: backend.temperature } ← binding
Canvas { roll: backend.roll } ← binding
Rectangle { height: backend.cpuPercent } ← binding
}
Same sensors, same keyboard fallback, same visual layout — different toolkit, different trade-offs.
1. Develop on Host
Just like the SDL2 dashboard, start by building and testing on your development machine. Qt6 runs on desktop Linux with the xcb (X11) or wayland platform plugin — the same QML code renders in a normal desktop window.
Install Host Dependencies
On Debian/Ubuntu:
sudo apt install qt6-base-dev qt6-declarative-dev \
qml6-module-qtquick qml6-module-qtquick-layouts \
qml6-module-qtquick-window cmake g++
On Fedora:
On Arch:
Build and Run Locally
No sudo, no QT_QPA_PLATFORM override — Qt auto-detects your desktop display server (X11 or Wayland) and opens a regular window. You'll see:
Use arrow keys to drive the gauges. The CPU bar shows your host's real CPU usage.
Checkpoint
The Qt dashboard opens in a desktop window. Arrow keys adjust simulated temperature and roll. The CPU bar updates in real time.
Tip
This is the key advantage of Qt for development: the same QML renders identically on desktop and embedded. You iterate on layout, colours, and animations in a fast desktop compile-run cycle, then switch to EGLFS for the final target deployment. No code changes needed.
Optional: Test in QEMU
For ARM-fidelity testing, you can run the Qt dashboard inside a QEMU system VM, the same way as described in SDL2 Dashboard § 1.
Build inside a QEMU system VM:
Boot a Pi OS image in QEMU with virtio-gpu, SSH in, install Qt6 inside the VM, build and run. The QEMU window shows the graphical output.
qemu-system-aarch64 \
-M virt -cpu cortex-a72 -m 2G -smp 4 \
-device virtio-gpu-pci \
-display gtk,gl=on \
-device qemu-xhci -device usb-kbd \
-drive file=pi-image.qcow2,if=virtio \
-nic user,hostfwd=tcp::2222-:22 \
-bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
Inside the VM, set QT_QPA_PLATFORM=eglfs to test the EGLFS path with the virtual GPU — this validates the same rendering path as the Pi.
Tip
Cross-compilation for Qt is more involved than for SDL2 (Qt requires a cross-compiled sysroot with all Qt modules). For most students, host-native development is sufficient — reserve QEMU for validating the full EGLFS path or testing Buildroot images.
2. Install Qt6 (Target)
On the Raspberry Pi, install the Qt6 development packages:
sudo apt install qt6-base-dev qt6-declarative-dev \
qml6-module-qtquick qml6-module-qtquick-layouts \
qml6-module-qtquick-window cmake g++
Verify the installation:
Checkpoint
qmake6 --version prints Qt 6.x.
Tip
The total Qt6 Quick runtime is about 80 MB of shared libraries. Compare this to SDL2's ~2 MB. This is one side of the trade-off: Qt brings a full UI framework, but at a cost in storage and RAM.
3. Understand EGLFS
EGLFS (EGL Full Screen) is Qt's platform plugin for embedded systems. It renders directly to the display using EGL and OpenGL ES through the DRM/KMS subsystem — the same kernel path that SDL2 uses with SDL_VIDEODRIVER=kmsdrm.
SDL2 path Qt EGLFS path
┌──────────────┐ ┌──────────────┐
│ Application │ │ Application │
│ (C code) │ │ (QML + C++) │
├──────────────┤ ├──────────────┤
│ SDL2 │ │ Qt Quick │
│ kmsdrm │ │ Scene Graph │
├──────────────┤ ├──────────────┤
│ │ │ EGL/GLES │
│ DRM/KMS │ │ DRM/KMS │
│ (kernel) │ │ (kernel) │
└──────────────┘ └──────────────┘
Key points:
- No compositor needed — EGLFS owns the entire screen (one fullscreen window)
- GPU required — EGLFS needs OpenGL ES support (the Pi 4's V3D GPU provides this)
- Set via environment —
QT_QPA_PLATFORM=eglfstells Qt to use this plugin - Same kernel path — both SDL2 and Qt ultimately use DRM/KMS for scanout
Warning
EGLFS supports only one fullscreen window. You cannot have multiple top-level windows or popups. This is by design for embedded systems — one application owns the display.
4. Build and Run on Target
Copy the source to the Pi (or build directly if you cloned the repo there). Build with CMake:
Checkpoint
The binary build/qt_dashboard exists.
Stuck?
- If CMake cannot find Qt6: ensure
qt6-base-devandqt6-declarative-devare installed - If QML modules are missing: install
qml6-module-qtquickandqml6-module-qtquick-layouts - If linking fails: check that
cmakeversion is ≥ 3.16 (cmake --version)
Launch with the EGLFS platform:
Warning
QT_QPA_PLATFORM=eglfs is required for direct rendering without a compositor. On your development host you didn't need this because Qt auto-detected X11/Wayland — on the Pi there is no compositor, so EGLFS renders directly to the GPU.
You should see the same three-panel dashboard as on your host — and as the SDL2 version: temperature arc on the left, horizon in the center, CPU bar on the right.
If sensors are connected (MCP9808, BMI160), the gauges show real data. Otherwise, keyboard fallback works the same way:
- Up/Down — adjust simulated temperature
- Left/Right — adjust simulated roll
- Escape — quit
Checkpoint
The Qt dashboard renders fullscreen on HDMI via EGLFS. If sensors are present, gauges show real data. The UI looks identical to the desktop version from Section 1.
5. Read the Code (Host or Target)
The Qt dashboard splits into three files with distinct responsibilities:
main.cpp — Sensor Backend (C++)
The SensorBackend class reads the same sensor paths as the SDL2 version:
class SensorBackend : public QObject
{
Q_OBJECT
Q_PROPERTY(float temperature READ temperature NOTIFY temperatureChanged)
Q_PROPERTY(float roll READ roll NOTIFY rollChanged)
Q_PROPERTY(float cpuPercent READ cpuPercent NOTIFY cpuPercentChanged)
// ...
};
Q_PROPERTY is the bridge between C++ and QML. When the timer fires and reads a new temperature, emitting temperatureChanged() causes every QML binding that references backend.temperature to re-evaluate automatically.
main.qml — UI Layout (QML)
The layout is declarative — you describe what to show, not how to draw it:
RowLayout {
GaugeArc { value: backend.temperature; minValue: 15; maxValue: 45 }
Canvas { property real roll: backend.roll; /* ... */ }
Rectangle { height: parent.height * backend.cpuPercent / 100 }
}
Compare this to the SDL2 version where every frame calls draw_arc(), SDL_RenderFillRect(), and SDL_RenderDrawLine() explicitly. In QML, the scene graph handles dirty-region tracking and only redraws what changed.
GaugeArc.qml — Custom Component
The arc gauge is a reusable QML component using Canvas for custom drawing:
Canvas {
onPaint: {
var ctx = getContext("2d");
ctx.arc(cx, cy, radius, startAngle, valueAngle);
ctx.stroke();
}
Connections {
target: gauge
function onValueChanged() { arcCanvas.requestPaint(); }
}
}
The Canvas API mirrors HTML5 Canvas — arc(), stroke(), fillRect(). Students familiar with JavaScript will recognise the pattern.
Tip
Notice the difference in adding a new gauge:
- SDL2: Write ~40 lines of C drawing code, integrate into the render loop
- Qt: Create a new QML file, bind it to a property, add it to the layout (~20 lines)
This is the development-effort trade-off described in Theory 3.
Checkpoint
You can identify the three-layer architecture: C++ backend (sensor reading) → Q_PROPERTY bridge → QML frontend (declarative layout + Canvas drawing).
6. Measure (on Target)
Record the same metrics as the SDL2 dashboard, measured on the Pi:
Binary Size
Expected: ~80 KB (the binary itself; Qt libraries are shared).
To see the full dependency chain:
Runtime RAM
sudo QT_QPA_PLATFORM=eglfs ./build/qt_dashboard &
sleep 3
smem -k -P qt_dashboard
# Or:
ps aux | grep qt_dashboard
Expected: ~35-50 MB RSS (Qt Quick scene graph, QML engine, OpenGL ES context).
Startup Time
time sudo QT_QPA_PLATFORM=eglfs ./build/qt_dashboard &
# Wait for the dashboard to appear, then kill
Expected: ~2-3 seconds to first frame (QML engine initialization, shader compilation on first run).
Checkpoint
Record your measurements in this table:
| Metric | Value |
|---|---|
| Binary size | |
| Runtime RAM (RSS) | |
| Startup time | |
| Shared libraries (count) |
7. Head-to-Head Comparison
Fill in this table using your measurements from SDL2 Dashboard § 6 and Section 6 above:
| Metric | SDL2 Dashboard | Qt Dashboard | Winner |
|---|---|---|---|
| Binary size | ~50 KB | ~80 KB (+ Qt libs) | |
| Runtime RAM | ~5-8 MB | ~35-50 MB | |
| Startup time | < 0.5 s | ~2-3 s | |
| Lines of code | ~280 (C) | ~40 (C++) + ~120 (QML) | |
| GPU required | No (software fallback) | Yes (EGLFS needs EGL) | |
| Adding a new gauge | ~40 lines C | ~20 lines QML | |
| Touch/input handling | Manual SDL events | Built-in MouseArea | |
| Text rendering | External lib (SDL2_ttf) | Built-in (Text element) |
Tip
There is no universal "winner" — the right choice depends on your project constraints:
- RAM-constrained, simple UI → SDL2 (industrial sensor display, boot splash)
- Complex UI, rapid iteration → Qt (vehicle dashboard, medical monitor, kiosk)
- No GPU → SDL2 (can use software renderer)
- Touch interface → Qt (built-in gesture recognition, animations)
Checkpoint
Your comparison table is filled in with real measurements from your Pi. You can articulate when to choose each toolkit.
What Just Happened?
You built the same dashboard with two fundamentally different approaches:
SDL2 gives you a thin rendering layer. You write C code that explicitly draws every pixel, every frame. The result is tiny (5 MB RAM) and fast to start (< 0.5 s), but every new feature means more manual drawing code.
Qt + EGLFS gives you a complete UI framework. You declare the layout in QML, bind values to C++ data sources, and the scene graph handles rendering. The result uses more RAM (~40 MB) and takes longer to start (~2 s), but new features are just a few lines of QML.
Both render through the same kernel subsystem (DRM/KMS) and produce the same visual output. The difference is in development model and runtime cost — exactly the trade-off described in Theory 3.
Challenges
Challenge 1: Touch Navigation
Add touch input to the Qt dashboard: tapping a gauge panel should expand it to full width (hiding the other two panels). Implement this using QML MouseArea, states, and transitions. This demonstrates Qt's built-in animation system — something that would require significant manual code in SDL2.
Challenge 2: Animated Transitions
Add a fade transition when switching between gauge views. Use QML Behavior on opacity with a NumberAnimation. Measure whether the animation affects frame timing or CPU usage.
Deliverable
- [ ] Running Qt dashboard on EGLFS with all three panels
- [ ] Measurement table completed (binary size, RAM, startup time)
- [ ] Head-to-head comparison table filled in with real values
- [ ] Written answer: "For a battery-powered industrial sensor with 64 MB RAM, which toolkit would you choose and why?"