Skip to content

About

Embedded Wayland server (smithay) for ivi-homescreen: runs Wayland clients inside Flutter platform views as zero-copy layers, with no GPU context of its own

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

ihs_wl_server

An embedded Wayland server for ivi-homescreen. It lets a Flutter app host ordinary Wayland clients as platform views: each client toplevel is shown inside a widget and composed with the rest of the Flutter scene.

The server is built on smithay. It is a shared library that the Flutter app loads over Dart FFI into the shell's process. The shell remains the only compositor. This module never renders and has no GPU context of its own. It hands each client buffer, as a dma-buf, to the shell's platform-view layer interface. The shell then composites it like any Flutter layer or puts it directly on a display plane.

Status

Early development. Working today:

  • A Wayland socket with wl_compositor, wl_subcompositor, xdg_wm_base, wl_shm, wp_single_pixel_buffer_manager_v1, zwp_linux_dmabuf_v1, wp_viewporter, wp_presentation, wp_fifo_manager_v1, wp_commit_timing_manager_v1, wp_linux_drm_syncobj_manager_v1 (when a render node supports it), wl_seat, wp_cursor_shape_manager_v1, xdg_activation_v1, wl_data_device_manager, wl_output and zxdg_output_manager_v1.
  • A view binds to a toplevel by activation token or app_id and configures it to the view's size, or to a size the view requests, scaled to fit; a window the client makes larger than asked (a minimum size wider than the view) is scaled down to fit rather than cut off. The toplevel is tiled on every edge (no borders or shadows) and maximized or fullscreen only when the app grants it (ihs_wl_view_state). xdg_activation_v1 carries the token: a client started with one the module issued (ihs_wl_activation_token) in XDG_ACTIVATION_TOKEN activates its toplevel with it, as GTK and Qt do.
  • A toplevel's surface tree (the root and its subsurfaces, with viewports, buffer transforms and opaque regions) reaches the shell as one layer per surface, bottom to top. Each layer keeps a stable id, so it keeps its display plane while the tree changes.
  • wl_shm surfaces (ARGB8888 and XRGB8888) are copied into linear dma-bufs the server owns; only what changed since a staging buffer was last filled is copied. They come from a contiguous (CMA) dma-heap when there is one, so a display that scans out only contiguous memory (the Raspberry Pi 5) can put them on a plane, else from gbm on a render node. IHS_WL_STAGING_HEAP names a heap under /dev/dma_heap; IHS_WL_STAGING_DEVICE names a render node and forces gbm. A single-pixel buffer goes the same way, as a 1x1 buffer of its color for the shell to stretch.
  • Commits are held until their dma-bufs are ready, so the shell never needs an acquire fence. With explicit sync the commit's acquire point decides; otherwise the buffer's implicit fence does. A buffer's release point is signaled when it is released.
  • Buffers are released to the client when the shell's release fence signals. Without a fence, a buffer is released once a later frame is on screen. Frame callbacks fire when a frame is shown.
  • zwp_linux_dmabuf_v1 v4 with default feedback naming the shell's render device as main device, so Mesa's Wayland EGL and Vulkan, GTK and Qt render on the GPU the shell imports on. A shell that cannot tell its device gets v3.
  • Per-surface feedback with a scanout tranche. When the shell reports that a surface's layer could go on a KMS plane but for its buffer's format, that surface's feedback gets a scanout tranche for the KMS device, ahead of the main one: the planes' formats that the shell also imports. The client allocates its next buffers for the plane, and the layer bypasses composition. The tranche stays while the view shows the surface.
  • Frame timing follows the shell's reports of shown frames. Presentation feedback carries the display's time, refresh, counter and flags (including zero-copy). A fifo barrier clears once the update that set it is shown. A commit with a target time is applied one refresh ahead of the first vblank at or after it. For views off screen, a clock stands in for the reports: what waits clears at the next refresh, then about once a second, so a fifo client that ignores suspended makes progress without rendering at the display's rate. Commit-timing targets are still honored.
  • A toplevel is activated only while its view has keyboard focus and is in the scene. When the view leaves the scene (scrolled off, occluded, its display gone), the toplevel is suspended (xdg-shell v6) so the client can stop rendering, and its frame callbacks wait until it is back.
  • Popups (menus, tooltips) show above their toplevel in its view, placed inside it as their positioner allows. A popup that grabs keeps input on its client until a press lands outside it; popups also close when their view loses keyboard focus or leaves the scene.
  • Dialogs (toplevels with a parent: About, file choosers, message boxes) show in their parent's view, centered over it and above it. The topmost window has the keyboard and is the activated one, and a dialog never binds a view of its own.
  • Scale follows the view's device pixel ratio (the widget passes it at creation). The toplevel is configured in logical pixels, and every surface is told the ratio through wp_fractional_scale_v1, rounded up through wl_surface.preferred_buffer_scale, and through the output it enters. A client that renders at it attaches buffers the size of the view on screen, which the shell shows 1:1.
  • Pointer, touch and keyboard input go straight from the Dart view to the module over FFI, in view-local logical pixels. What is under a point is hit-tested against the tree the view shows, input regions included. The view takes keyboard focus when pressed; keys are sent as evdev codes and clients repeat them from wl_keyboard.repeat_info (600 ms, 25 Hz). The keymap is xkbcommon's default, set by the XKB_DEFAULT_* variables, as the shell's own is.
  • The mouse cursor over a view is the one its client asks for. The module draws none: ihs_wl_pointer returns the client's wp_cursor_shape_v1 shape, and the view shows it as the matching Flutter cursor. A client that sets a cursor surface of its own gets the default arrow; one that hides the cursor (IHS_WL_CURSOR_HIDDEN) hides it.

With the egl-wl-display cargo feature (off by default), the server runs on libwayland-server and binds an EGL display to it (EGL_WL_bind_wayland_display), for EGL implementations whose Wayland clients share buffers over a protocol of their own rather than linux-dmabuf. The EGL library serves that protocol. On a shell that samples image layers (an EGL backend, platform-view ABI 1.16) the display bound is the shell's own, and each buffer goes to it as an EGLImage, so nothing about the buffer is lost, compression included. Elsewhere each buffer is turned into a dma-buf through libgbm, which needs one that can describe such buffers (one exporting gbm_perform, as CodeLinaro libgbm does); some implementations allocate compressed buffers by default, which can show corrupted that way, so have the client's EGL share them uncompressed there (the server logs a warning when it sees one). The feature links libwayland-server and loads libEGL at run time; there is still no EGL context. An app turns the feature on in its pubspec:

hooks:
  user_defines:
    ihs_wayland_server:
      cargo_features: [egl-wl-display]

Layout

Path What
src/ The server, and the C ABI in src/lib.rs
include/ihs_wl.h The C header, generated by cbindgen
dart/ihs_wayland_server/ Dart package: WaylandServer and WaylandToplevelView
examples/standalone.rs Runs the server without a shell, for trying real clients
tests/ Integration tests against a mock shell and a Wayland client

Building

This crate needs:

  • Rust 1.94.1 or newer. rust-toolchain.toml pins 1.94.1, the oldest compiler a supported Yocto release ships, so nothing newer creeps in.
  • ivi-homescreen's shared library (libihs_shared), at platform-view ABI 1.16 or newer. The build finds it through ivi-homescreen-shared.pc, checks the ABI in its headers, and generates bindings from them.
  • clang, for bindgen.

To install the shared library to a local prefix and build against it:

cmake -S <ivi-homescreen>/shared -B build-shared -G Ninja \
  -DCMAKE_INSTALL_PREFIX=$PWD/prefix -DENABLE_DLT=OFF
ninja -C build-shared install

export PKG_CONFIG_PATH=$PWD/prefix/lib64/pkgconfig   # or lib/pkgconfig
cargo build --release

The output is target/release/libihs_wl_server.so.

For a native build against a prefix outside /usr, the build embeds an rpath, so cargo test runs without LD_LIBRARY_PATH. Cross builds and sysroot builds never get an rpath. To cross-compile, set PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH as for any pkg-config consumer. In Yocto, the cargo bbclass sets them for you.

Yocto

meta-flutter builds the library per app, by the package's build hook, and it ships in the app's bundle at the version the app's pubspec pins, so apps on one image can each carry their own. The app recipe inherits flutter-app-native with FLUTTER_NATIVE_CARGO = "1", which vendors the crates while the pub cache is fetched and gives the hook a cargo wrapper for the target, and depends on what the crate links:

DEPENDS += "ivi-homescreen-shared libdrm libxkbcommon virtual/libgbm"
FLUTTER_NATIVE_CARGO = "1"
inherit flutter-app-native

meta-flutter's ihs-wl-server-example recipe builds the example this way. The Rust each release needs:

Release Rust Layers
master 1.98.1 oe-core
wrynose 1.94.1 oe-core
scarthgap 1.98.1 meta-lts-mixins scarthgap/rust and meta-clang; a recipe building the crate belongs in a dynamic layer on the mixin

Using it from Flutter

Add the Dart package, start the server once, and place a view where the client should appear:

import 'package:ihs_wayland_server/ihs_wayland_server.dart';

final server = WaylandServer.start(); // loads the bundled libihs_wl_server.so
// Launch clients with WAYLAND_DISPLAY=${server.socketName}.

const WaylandToplevelView(appId: 'org.example.app');

A view shows the oldest toplevel with a matching app_id that no other view is already showing, or with no app_id given, the oldest one at all. It stays empty until such a client maps.

To show the very client you start, whatever else runs with the same app_id, launch it with an activation token and name the token in its view:

final (:process, :token) = await server.launch('gnome-calculator', []);

WaylandToplevelView(activationToken: token);

launch sets WAYLAND_DISPLAY and XDG_ACTIVATION_TOKEN for the client; server.activationToken() gives a token for a launcher of your own. The client activates its toplevel with the token (GTK and Qt do this unprompted), and the view binds that toplevel. Views binding by app_id pass over toplevels launched with a token. A client that ignores XDG_ACTIVATION_TOKEN is never found by one; show it by app_id instead.

The package's build hook builds the crate with cargo as part of the app's build and bundles the library, so there is nothing to build by hand. The hook runs with a filtered environment: a cross build puts a cargo wrapper carrying the target, linker and pkg-config settings first on PATH (emb does this), and ivi-homescreen-shared.pc outside the default search path is named in the app's pubspec:

hooks:
  user_defines:
    ihs_wayland_server:
      pkg_config_path: /path/to/prefix/lib/pkgconfig

dart/ihs_wayland_server/example is a minimal app; its emb.yaml builds it with emb against ivi-homescreen.

A view configures its client to the view's size and follows every layout change. WaylandToplevelView(requestedSize: Size(800, 600)) asks the client for that size instead; its content is scaled to fit the view, aspect kept, centered, and input is mapped back (ihs_wl_view_size in the C ABI).

A WaylandToplevelController reports what happens to the toplevel a view shows and closes it:

late final WaylandToplevelController controller = WaylandToplevelController(
  // The client shows buttons for these and no others; none by default.
  windowCapabilities: {WaylandWindowCapability.maximize},
  onWindowRequest: (request) {
    // maximize, unmaximize (and minimize, fullscreen, unfullscreen when
    // handled): the client's own buttons and keys. Grant the state so the
    // client draws itself so, and size the view as the app sees fit.
    if (request == WaylandWindowRequest.maximize) {
      controller.windowState = {WaylandWindowState.maximized};
    } else if (request == WaylandWindowRequest.unmaximize) {
      controller.windowState = {};
    }
  },
);
controller.addListener(() {
  if (!controller.isBound) {
    // The client closed its window, or quit.
  }
});

WaylandToplevelView(activationToken: token, controller: controller);

controller.close(); // xdg_toplevel.close; the client may ask its user first

Events reach Dart over a native port (ihs_wl_set_event_port), which a hot restart re-registers; ihs_wl_view_close asks the client to close, and ihs_wl_view_capabilities sets the wm_capabilities its client is sent.

WaylandServer.start is idempotent. After a hot restart the server is still running. Its clients carry on, and the re-created views bind to their toplevels again.

By default the socket is named wayland-ihs-N when the shell is itself a Wayland client (the wayland backends), and wayland-N otherwise.

Trying it without a shell

cargo run --example standalone -- wayland-test 60
WAYLAND_DISPLAY=wayland-test foot

Clients connect and map, but nothing is shown, because there is no shell to host the views.

Explicit sync

Explicit sync is offered when a render node can wait on timeline syncobjs through eventfds (Linux 6.6 or newer). A syncobj file is not tied to one device, so the server uses the first /dev/dri/renderD* that can, whichever GPU clients render on. IHS_WL_SYNCOBJ_DEVICE names a render node to use instead. Without one, the global is not advertised and clients use implicit sync.

Logging

Logging uses tracing. The filter comes from IHS_WL_LOG, in EnvFilter syntax; the default is info,smithay=warn.

Testing

cargo test
cargo clippy --all-targets
cargo fmt --check

The integration tests run the server against a mock of the shell's platform-view interface and drive it with a real Wayland client. In place of dma-bufs, the client uses memfds; the server never reads buffer contents.

docs/development.md covers the Dart package's checks, and running the example app with real clients (GTK 4, Chromium, mpv) on a Raspberry Pi 5.

License

Apache-2.0; see LICENSE.

About

Embedded Wayland server (smithay) for ivi-homescreen: runs Wayland clients inside Flutter platform views as zero-copy layers, with no GPU context of its own

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages