Skip to content

Rework the OSD onto LVGL, drop Cairo#140

Open
henkwiedig wants to merge 16 commits into
OpenIPC:masterfrom
henkwiedig:lvgl-osd
Open

Rework the OSD onto LVGL, drop Cairo#140
henkwiedig wants to merge 16 commits into
OpenIPC:masterfrom
henkwiedig:lvgl-osd

Conversation

@henkwiedig

Copy link
Copy Markdown
Collaborator

Rework the OSD onto LVGL, drop Cairo

Replaces the Cairo-drawn OSD with LVGL widgets, and adds two new widgets on top of that
framework: a full-screen signal warning border and an MSP DisplayPort renderer. Also fixes the
alpha-compositing problems that surface once the OSD is drawn by LVGL.

16 commits, ~1700 insertions / ~580 deletions across 12 files.

Why

The OSD widgets each drew themselves imperatively into a Cairo surface every refresh, while the
menu was already an LVGL application. That meant two rendering stacks, two font systems, and a
full repaint of every widget on every tick regardless of what had changed. Moving the OSD onto
LVGL leaves one renderer, lets widgets keep persistent objects that are only touched when their
data changes, and drops the Cairo dependency from the runtime build.

What changed

Widget framework

Widget loses its draw(cairo_t*) entry point and gains two:

  • createLvObjects(parent, screen_w, screen_h) — build the LVGL objects once at startup.
  • tick() — update them, guarded by a dirty flag set from setFact(), so a widget whose facts
    haven't changed does no work.

Every existing widget (TextWidget, IconTextWidget, TplTextWidget, BoxWidget,
BarChartWidget, PopupWidget, GPSWidget, DvrStatusWidget, IconSelectorWidget, the video
stat widgets, …) was ported to this pair. Positioning helpers absX()/absY() keep the existing
negative-coordinate-wraps-from-the-far-edge behaviour.

The OSD renders into lv_layer_bottom() so it sits below the menu layer, into a full-screen
transparent container in ARGB8888. The render loop runs at ~60 Hz for smooth animation while
widget tick()s stay on the configured refresh_frequency_ms, and fact intake moved to its own
OSD-FactProc thread that drains the queue under the LVGL lock, so publishing a fact no longer
blocks on rendering.

New: SignalWarningWidget

Draws a red border around the video that fades in as the signal degrades, reaching full intensity
at critical. The widget's x/y are ignored — it always covers the screen.

  • Accepts multiple facts and reacts to the most optimistic reading among them, so you can
    bind every antenna's wfbcli.rx.ant_stats.rssi_avg (one per ant_id tag) and the border only
    appears once even the best antenna has dropped below threshold.
  • Direction is inferred from where critical sits relative to threshold: critical < threshold
    means lower is worse (RSSI), critical > threshold means higher is worse (fec_recovered). No
    extra config key.
  • Optional average_ms smooths the aggregated value through a time-windowed running average so
    brief spikes don't flash the border.
  • Optional rate mode treats the bound facts as byte/packet deltas and warns on the derived
    per-second rate (rate_divisor, e.g. 125000 for Mbit/s), mirroring VideoBitrateWidget. This is
    what the shipped config_osd.json demo uses, warning on gstreamer.received_bytes.

New: MspDisplayPortWidget

Renders an MSP DisplayPort OSD from a flight controller (Betaflight / INAV / ArduPilot). Listens
for DisplayPort messages over UDP (udp_port, default 14551) and draws the character grid from a
bitmap font atlas at font_path, scaled to fill the screen.

  • Reader/parser and renderer are decoupled: the parser thread fills character_map, the render
    thread reads render_map, so a partially parsed frame never reaches the screen.
  • Honors SET_OPTIONS to switch grid resolution (INAV/ArduPilot/HDZero). Betaflight never sends
    it and stays on the 53x20 default.
  • Shows a font preview at startup and a "no data" message when nothing arrives on the port.

Straight-alpha compositing fixes

LVGL hands back straight (non-premultiplied) alpha. Three consumers needed to be told:

  1. DRM plane (drm.c) — the OSD plane defaults to Pre-multiplied blending, which composites
    coloured semi-transparent pixels wrong (colour not scaled by alpha; a red gradient renders
    solid). We now look up the pixel blend mode enum and set Coverage. Silently skipped if the
    driver doesn't expose the property.
  2. RGA blend (frame_processor.cpp) — the DVR overlay path now passes
    IM_ALPHA_BLEND_PRE_MUL alongside SRC_OVER so RGA premultiplies before blending.
  3. Empty CRTC (drm.c, main.cpp) — with no video, straight-alpha OSD pixels blend against an
    empty CRTC and come out solid. modeset_create_black_video() allocates a full-screen black
    NV12 frame, and the display thread attaches it to the video plane after 5 s without a frame
    (also true at startup), restoring the aspect-scaled geometry when video resumes. The geometry
    maths was factored out into modeset_set_video_geometry(), shared by both paths.

Fact flushing on RX mode switch

osd_flush_facts() resets all facts to undefined; the active sources re-publish theirs.
apply_rx_mode() calls it when the receiver mode actually changes, which drops the previous mode's
stale facts (os_mon.wifi.rssi lingering after apfpv, wfb ant_stats after wfb).

This replaces the hack in gs_connection_checker.c that published deliberately out-of-bounds
values for ten antennas to force the OSD bars to hide. update_network_status() is now just the
tunnel-byte counter refresh that apfpv drone detection needs.

Build

Cairo is dropped from the runtime build — it's now only pulled in for BUILD_TESTS, where the old
draw(cairo_t*) paths survive under #ifdef TEST to keep the existing widget tests compiling.
LV_FONT_MONTSERRAT_20 is enabled for the OSD container's default font.

Config

config_osd.json gains a working SignalWarning demo (rate mode on gstreamer.received_bytes)
plus MspDisplayPort and DebugWidget entries left disabled via the --- type prefix. README
documents both new widgets.

henkwiedig and others added 16 commits July 11, 2026 12:45
drop-cairo-2 reconciled onto master via a 3-way squash-merge (base
b0d261c). The column-menu refactor and DVR-fps feature this branch
started already landed upstream (OpenIPC#134-OpenIPC#138) in a more polished form, so
this keeps master's versions of those and applies only the branch's
genuinely new work on top:

- osd.cpp: widget framework (BarChart, SignalWarning, Popup w/ fade,
  DebugWidget), facts processor thread, MSP DisplayPort & ExternalSurface
  widgets, tighter OSD loop, decoupled MSP reader/parser
- drm.c, frame_processor.cpp: supporting changes (merged with master's)
- CMakeLists.txt, lv_conf.h, README.md

Master's post-fork fixes are preserved: gstrtpreceiver codec detection,
main.cpp frame-discard/scheduling, colmenu prefetch + ensure_escapable,
rec_enabled 1/0 fix, openipc_spinner_create.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant