diff --git a/utils/plymouth-plugin-ucode/Makefile b/utils/plymouth-plugin-ucode/Makefile new file mode 100644 index 0000000..fe52617 --- /dev/null +++ b/utils/plymouth-plugin-ucode/Makefile @@ -0,0 +1,69 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=plymouth-plugin-ucode +PKG_VERSION:=1 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=Daniel Golle +PKG_LICENSE:=GPL-2.0-or-later + +include $(INCLUDE_DIR)/package.mk + +define Package/plymouth-plugin-ucode + SECTION:=utils + CATEGORY:=Utilities + TITLE:=ucode-driven plymouth splash plugin + URL:=https://www.freedesktop.org/wiki/Software/Plymouth/ + DEPENDS:=+plymouth +libucode +plutovg +ucode-mod-plutovg +libubus \ + +libubox +libblobmsg-json +endef + +define Package/plymouth-plugin-ucode/description + A plymouth splash plugin that hands the screen to a ucode script. The + script draws with plutovg and decides what changed, so a theme is a + program rather than a sequence of rendered frames, and it can react to + ubus events while the system boots. +endef + +UCODE_SONAME:=$(notdir $(firstword $(wildcard $(STAGING_DIR)/usr/lib/libucode.so.*))) + +TARGET_CFLAGS += \ + -DUCODE_SONAME=\"$(UCODE_SONAME)\" \ + -I$(STAGING_DIR)/usr/include/plymouth-1/ply \ + -I$(STAGING_DIR)/usr/include/plymouth-1/ply-splash-core \ + -std=gnu99 -Wall -Wextra -Wno-unused-parameter + +define Build/Compile + $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) $(FPIC) \ + -ffunction-sections -fdata-sections -Wl,--gc-sections \ + -shared -o $(PKG_BUILD_DIR)/ucode.so \ + $(PKG_BUILD_DIR)/ucode-splash.c \ + -lply -lply-splash-core -lucode -lplutovg \ + -lubus -lubox -lblobmsg_json +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./src/* $(PKG_BUILD_DIR)/ +endef + +define Build/Configure +endef + +define Package/plymouth-plugin-ucode/install + $(INSTALL_DIR) $(1)/usr/lib/plymouth + $(INSTALL_BIN) $(PKG_BUILD_DIR)/ucode.so $(1)/usr/lib/plymouth/ + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/plymouth-theme-bench.uc \ + $(1)/usr/bin/plymouth-theme-bench + $(SED) 's|@VERSION@|$(PKG_VERSION)-r$(PKG_RELEASE)|' \ + $(1)/usr/bin/plymouth-theme-bench + $(INSTALL_DIR) $(1)/lib/upgrade + $(INSTALL_DATA) ./files/upgrade-plymouth.sh \ + $(1)/lib/upgrade/plymouth.sh + $(INSTALL_DIR) $(1)/lib/preinit + $(INSTALL_DATA) ./files/preinit-plymouth.sh \ + $(1)/lib/preinit/03_plymouth +endef + +$(eval $(call BuildPackage,plymouth-plugin-ucode)) diff --git a/utils/plymouth-plugin-ucode/files/README.md b/utils/plymouth-plugin-ucode/files/README.md new file mode 100644 index 0000000..6aed0e3 --- /dev/null +++ b/utils/plymouth-plugin-ucode/files/README.md @@ -0,0 +1,125 @@ +# Writing a ucode plymouth theme + +A theme is one ucode program. The plugin compiles it when the splash is +shown, calls the object it returns, and gives it a plutovg canvas over +plymouth's own pixel buffer. + +## The theme file + +`ScriptFile` in the `.plymouth` file points at it. The program runs once +and must **return** an object; top-level function declarations stay +local to the program and are not visible to the plugin. + + import * as pvg from 'plutovg'; + + function setup(mode) { } + function frame(t) { } + function paint(cv, x, y, w, h) { } + function event(name, detail) { } + + return { setup: setup, frame: frame, paint: paint, event: event }; + +`setup(mode)` runs once, with the mode plymouth was started in +(`boot-up`, `shutdown`, `reboot`, `system-upgrade`, ...). + +`frame(elapsed)` runs at `FrameRate`, default 30 Hz. It advances state +and declares what changed; it must not draw. + +The splash starts in preinit, before the overlay is mounted and before +there is a bus to watch, so a theme has two phases to fill. The shipped +one spends the first growing its graph, a node at a time, for as long as +the phase lasts; `ubus-connected` completes whatever is left of it at +once and opens the second, where the traffic drawn is the traffic on the +bus. + +`paint(canvas, x, y, w, h)` draws one damaged rectangle. The canvas is +clipped to it and is valid only for the duration of the call: keeping a +reference to it and drawing later writes into a buffer plymouth may have +moved on from. + +`event(name, detail)` receives what the system is doing, see below. + +## Damage is the contract + +Nothing reaches the screen unless the theme asks for it. Call +`plymouth.damage(x, y, w, h)` for every region that changed since the +last frame, including the region something moved *away* from. The plugin +turns each call into a `ply_pixel_display_draw_area()` and plymouth +calls `paint()` back for that region. + +Damaging the whole screen every frame works and is slow: a full 1080p +repaint of a busy scene costs about 27 ms against 0.06 ms for the +handful of boxes that actually changed. Damage tightly. + +## The plymouth object + + plymouth.screens() array of { width, height, scale } + plymouth.damage(x, y, w, h) + plymouth.mode() the mode string + plymouth.theme_dir() where the theme was installed + plymouth.elapsed() seconds since the splash appeared + plymouth.log(msg) to plymouth's trace output + +## Events + +`event(name, detail)`: + + setup-time mode passed to setup() instead + progress { duration, fraction } from plymouth + status a status line + output console output during boot + message a message plymouth wants shown + system-update an integer percentage, during sysupgrade + idle plymouth is handing the display over + ubus-connected the splash reached ubus + ubus { type, data, path, present } + +`ubus` covers both the live event stream and, with `present` set, the +objects that already existed when the splash connected. Objects that +registered before then are state rather than events and would otherwise +never be seen: on a Rock 5B `service`, `system` and every `hotplug.*` +object exist before preinit runs. + +`data` is the raw JSON of the event. For `ubus.object.add` the `path` +field is provided directly, so a theme does not have to parse it. + +## Pictograms + +A theme draws a pictogram per startup stage from its own +`pictograms/.svg`, named after the init script in `/etc/init.d`. +A package that is not part of the theme drops one in as + + /usr/share/plymouth/pictograms/.svg + +and needs nothing else: no registration, and no variable in the init +script. + +Which services exist is not knowable before the overlay is mounted, so +the shipped theme reads `/etc/rc.d/S*` once the bus comes up and shows a +stage, in start order, for every enabled service it finds a pictogram +for. Enabled but not yet started is drawn dark; the stage lights when +the service's own ubus object appears. A service that is not installed +is never drawn, and neither is one with no pictogram. Nothing at all is +drawn before that: the first phase is the graph by itself. + +A stage also appears under a bare ubus object name, so a service that +rc.d does not start can still be shown by a pictogram named after its +object. + +Some services announce nothing on the bus at all. sysntpd is the usual +one: it reports a sync by calling `hotplug.ntp`, which is a ubus method +call rather than a broadcast, so no listener ever sees it. Such a +service reports itself with a status line instead, + + plymouth update --status="service:" + +from wherever it does have a hook, and the theme lights that stage. The +shipped theme installs exactly that as /etc/hotplug.d/ntp/00-plymouth. + +## Measuring + +`plymouth-theme-bench [width] [height] [frames] [png]` runs +a theme against an offscreen surface through the same contract and +reports setup, frame and paint cost with the share of the screen +repainted per frame. It needs no display, so a theme can be compared +across targets. diff --git a/utils/plymouth-plugin-ucode/files/plymouth-theme-bench.uc b/utils/plymouth-plugin-ucode/files/plymouth-theme-bench.uc new file mode 100644 index 0000000..e505d5a --- /dev/null +++ b/utils/plymouth-plugin-ucode/files/plymouth-theme-bench.uc @@ -0,0 +1,179 @@ +#!/usr/bin/env ucode + +import * as pvg from 'plutovg'; + +const DEFAULT_FRAMES = 120; +const VERSION = '@VERSION@'; + +function usage(code) { + print(`plymouth-theme-bench ${VERSION}\n`); + print("usage: plymouth-theme-bench [theme dir] [width] [height] " + + "[frames] [png]\n"); + exit(code); +} + +switch (ARGV[0]) { +case '--version': +case '-version': +case 'version': +case '-v': +case '-V': + usage(0); +case '--help': +case '-help': +case '-h': +case '-?': + usage(0); +} + +let theme_dir = ARGV[0] ?? '/usr/share/plymouth/themes/openwrt-demo'; +let width = int(ARGV[1] ?? 1920); +let height = int(ARGV[2] ?? 1080); +let frames = int(ARGV[3] ?? DEFAULT_FRAMES); +let png_out = ARGV[4]; + +let damage = []; +let started = 0.0; + +function now() { + let t = clock(true); + + return t[0] + t[1] / 1000000000.0; +} + +function rect_merge(list) { + let out = []; + let keep; + + for (let i = 0; i < length(list); i++) { + keep = true; + + for (let j = 0; j < length(out); j++) { + if (list[i].x >= out[j].x && list[i].y >= out[j].y && + list[i].x + list[i].w <= out[j].x + out[j].w && + list[i].y + list[i].h <= out[j].y + out[j].h) { + keep = false; + break; + } + } + + if (keep) + push(out, list[i]); + } + + return out; +} + +global.plymouth = { + screens: function() { + return [ { width: width, height: height, scale: 1 } ]; + }, + + damage: function(x, y, w, h) { + push(damage, { x: x, y: y, w: w, h: h }); + + return true; + }, + + mode: function() { + return 'boot-up'; + }, + + theme_dir: function() { + return theme_dir; + }, + + elapsed: function() { + return now() - started; + }, + + log: function(msg) { + warn(`theme: ${msg}\n`); + } +}; + +started = now(); + +let script = theme_dir + '/openwrt.uc'; +let entry = loadfile(script); +let theme; + +if (!entry) { + warn(`cannot load ${script}\n`); + exit(1); +} + +theme = entry(); + +if (type(theme) != 'object' || type(theme.paint) != 'function') { + warn(`${script} returned no theme object\n`); + exit(1); +} + +let surface = pvg.surface(width, height); +let canvas = pvg.canvas(surface); +let setup_start = now(); + +theme.setup('boot-up'); + +let setup_ms = (now() - setup_start) * 1000.0; +let full = rect_merge(damage); +let first_start = now(); + +for (let i = 0; i < length(full); i++) + theme.paint(canvas, full[i].x, full[i].y, full[i].w, full[i].h); + +let first_ms = (now() - first_start) * 1000.0; + +let frame_total = 0.0; +let paint_total = 0.0; +let pixels = 0; +let t0, t1, rects; + +const EVENT_EVERY = 20; + +if (type(theme.event) == 'function') + theme.event('ubus-connected', null); + +for (let f = 0; f < frames; f++) { + damage = []; + + if (type(theme.event) == 'function' && f % EVENT_EVERY == 0) { + theme.event('ubus', { type: 'ubus.object.add', + data: '{"path":"hostapd.phy0"}' }); + theme.event('progress', { duration: 20.0, + fraction: f / (frames * 1.0) }); + } + + t0 = now(); + theme.frame(f / 30.0); + t1 = now(); + frame_total += t1 - t0; + + rects = rect_merge(damage); + t0 = now(); + + for (let i = 0; i < length(rects); i++) { + theme.paint(canvas, rects[i].x, rects[i].y, rects[i].w, rects[i].h); + pixels += rects[i].w * rects[i].h; + } + + paint_total += now() - t0; +} + +printf("theme %s\n", theme_dir); +printf("geometry %dx%d, %d frames\n", width, height, frames); +printf("setup %8.2f ms (scene build and static render)\n", setup_ms); +printf("first draw %8.2f ms (full screen)\n", first_ms); +printf("frame() %8.3f ms/frame\n", frame_total * 1000.0 / frames); +printf("paint() %8.3f ms/frame\n", paint_total * 1000.0 / frames); +printf("total %8.3f ms/frame %7.1f fps\n", + (frame_total + paint_total) * 1000.0 / frames, + frames / (frame_total + paint_total)); +printf("repainted %8.2f %% of the screen per frame\n", + 100.0 * pixels / frames / (width * height)); + +if (png_out) { + surface.write_png(png_out); + printf("wrote %s\n", png_out); +} diff --git a/utils/plymouth-plugin-ucode/files/preinit-plymouth.sh b/utils/plymouth-plugin-ucode/files/preinit-plymouth.sh new file mode 100644 index 0000000..9a359cc --- /dev/null +++ b/utils/plymouth-plugin-ucode/files/preinit-plymouth.sh @@ -0,0 +1,46 @@ +plymouth_preinit_devnodes() { + local node name major minor + + for node in /sys/class/drm/*/dev; do + [ -e "$node" ] || continue + + name="${node%/dev}" + name="${name##*/}" + + case "$name" in + *-*) continue ;; + esac + + [ -e "/dev/dri/$name" ] && continue + + IFS=: read major minor < "$node" + mkdir -p /dev/dri + mknod "/dev/dri/$name" c "$major" "$minor" 2>/dev/null + done +} + +plymouth_preinit_start() { + local card found + + [ -x /usr/sbin/plymouthd ] || return 0 + + plymouth_preinit_devnodes + + for card in /dev/dri/card*; do + [ -e "$card" ] && found="$card" + done + + [ -n "$found" ] || return 0 + + /usr/sbin/plymouthd --mode=boot --ignore-serial-consoles \ + --kernel-command-line="$(cat /proc/cmdline) splash" || return 0 + + /usr/bin/plymouth show-splash +} + +plymouth_preinit_failsafe() { + /usr/bin/plymouth quit 2>/dev/null +} + +boot_hook_add preinit_main plymouth_preinit_start +boot_hook_add failsafe plymouth_preinit_failsafe diff --git a/utils/plymouth-plugin-ucode/files/upgrade-plymouth.sh b/utils/plymouth-plugin-ucode/files/upgrade-plymouth.sh new file mode 100644 index 0000000..6d5d7f2 --- /dev/null +++ b/utils/plymouth-plugin-ucode/files/upgrade-plymouth.sh @@ -0,0 +1,18 @@ +[ -x /usr/bin/plymouth ] || return 0 + +RAMFS_COPY_BIN="$RAMFS_COPY_BIN /usr/bin/plymouth /usr/sbin/plymouthd" +RAMFS_COPY_DATA="$RAMFS_COPY_DATA /usr/lib/plymouth /usr/share/plymouth \ + /etc/plymouth /usr/lib/ucode /usr/lib/libplutovg.so.* \ + /usr/lib/libplutosvg.so.* /usr/lib/libply*.so.*" + +/usr/bin/plymouth --ping 2>/dev/null || return 0 + +/usr/bin/plymouth change-mode --system-upgrade + +# common.sh sorts before this file, so this replaces its v() rather than +# being replaced by it +v() { + _v "$(date) upgrade: $@" + logger -p info -t upgrade "$@" + /usr/bin/plymouth update --status="$*" 2>/dev/null +} diff --git a/utils/plymouth-plugin-ucode/src/ucode-splash.c b/utils/plymouth-plugin-ucode/src/ucode-splash.c new file mode 100644 index 0000000..0fab7d2 --- /dev/null +++ b/utils/plymouth-plugin-ucode/src/ucode-splash.c @@ -0,0 +1,931 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include "ply-boot-splash-plugin.h" +#include "ply-event-loop.h" +#include "ply-key-file.h" +#include "ply-list.h" +#include "ply-logger.h" +#include "ply-pixel-buffer.h" +#include "ply-pixel-display.h" +#include "ply-region.h" +#include "ply-rectangle.h" +#include "ply-trigger.h" + +#define DEFAULT_FRAME_RATE 30.0 + +typedef struct +{ + ply_pixel_display_t *display; +} view_t; + +#define UBUS_SOCKET_DIR "/var/run/ubus" + +struct _ply_boot_splash_plugin +{ + struct ubus_event_handler ubus_listener; + ply_boot_splash_mode_t mode; + struct ubus_context *ubus; + ply_fd_watch_t *ubus_watch; + ply_fd_watch_t *wait_watch; + ply_event_loop_t *loop; + ply_list_t *views; + int wait_fd; + uc_parse_config_t config; + uc_program_t *program; + uc_source_t *source; + uc_vm_t vm; + char *script_path; + char *theme_dir; + double frame_interval; + double started_at; + bool vm_ready; + bool animating; +}; + +static ply_boot_splash_plugin_t *plugin_instance; + + +static double +now_seconds (void) +{ + struct timespec ts; + + clock_gettime (CLOCK_MONOTONIC, &ts); + + return ts.tv_sec + ts.tv_nsec / 1000000000.0; +} + +static const char * +mode_name (ply_boot_splash_mode_t mode) +{ + switch (mode) { + case PLY_BOOT_SPLASH_MODE_BOOT_UP: + return "boot-up"; + case PLY_BOOT_SPLASH_MODE_SHUTDOWN: + return "shutdown"; + case PLY_BOOT_SPLASH_MODE_REBOOT: + return "reboot"; + case PLY_BOOT_SPLASH_MODE_UPDATES: + return "updates"; + case PLY_BOOT_SPLASH_MODE_SYSTEM_UPGRADE: + return "system-upgrade"; + case PLY_BOOT_SPLASH_MODE_FIRMWARE_UPGRADE: + return "firmware-upgrade"; + case PLY_BOOT_SPLASH_MODE_SYSTEM_RESET: + return "system-reset"; + default: + return "invalid"; + } +} + +static uc_value_t * +script_theme (ply_boot_splash_plugin_t *plugin) +{ + if (!plugin->vm_ready) + return NULL; + + return uc_vm_registry_get (&plugin->vm, "plymouth.theme"); +} + +static void +script_call (ply_boot_splash_plugin_t *plugin, const char *name, + uc_value_t **args, size_t nargs) +{ + uc_value_t *theme; + uc_value_t *fn; + size_t i; + + theme = script_theme (plugin); + fn = theme ? ucv_object_get (theme, name, NULL) : NULL; + + if (!ucv_is_callable (fn)) { + for (i = 0; i < nargs; i++) + ucv_put (args[i]); + + return; + } + + uc_vm_stack_push (&plugin->vm, ucv_get (theme)); + uc_vm_stack_push (&plugin->vm, ucv_get (fn)); + + for (i = 0; i < nargs; i++) + uc_vm_stack_push (&plugin->vm, args[i]); + + if (uc_vm_call (&plugin->vm, true, nargs) == EXCEPTION_NONE) + ucv_put (uc_vm_stack_pop (&plugin->vm)); + else + ply_error ("ucode splash: %s() raised an exception", name); +} + +static void +event_emit (ply_boot_splash_plugin_t *plugin, const char *name, + uc_value_t *detail) +{ + uc_value_t *args[2]; + + args[0] = ucv_string_new (name); + args[1] = detail; + + script_call (plugin, "event", args, 2); +} + +static uc_value_t * +uc_plymouth_damage (uc_vm_t *vm, size_t nargs) +{ + ply_boot_splash_plugin_t *plugin = plugin_instance; + ply_list_node_t *node; + int x, y, w, h; + view_t *view; + + if (!plugin) + return NULL; + + x = ucv_to_integer (uc_fn_arg (0)); + y = ucv_to_integer (uc_fn_arg (1)); + w = ucv_to_integer (uc_fn_arg (2)); + h = ucv_to_integer (uc_fn_arg (3)); + + if (w <= 0 || h <= 0) + return ucv_boolean_new (false); + + node = ply_list_get_first_node (plugin->views); + + while (node != NULL) { + view = ply_list_node_get_data (node); + ply_pixel_display_draw_area (view->display, x, y, w, h); + node = ply_list_get_next_node (plugin->views, node); + } + + return ucv_boolean_new (true); +} + +static uc_value_t * +uc_plymouth_screens (uc_vm_t *vm, size_t nargs) +{ + ply_boot_splash_plugin_t *plugin = plugin_instance; + ply_list_node_t *node; + uc_value_t *entry; + uc_value_t *list; + view_t *view; + + if (!plugin) + return NULL; + + list = ucv_array_new (vm); + node = ply_list_get_first_node (plugin->views); + + while (node != NULL) { + view = ply_list_node_get_data (node); + entry = ucv_object_new (vm); + + ucv_object_add (entry, "width", + ucv_int64_new (ply_pixel_display_get_width (view->display))); + ucv_object_add (entry, "height", + ucv_int64_new (ply_pixel_display_get_height (view->display))); + ucv_object_add (entry, "scale", + ucv_int64_new (ply_pixel_display_get_device_scale (view->display))); + + ucv_array_push (list, entry); + node = ply_list_get_next_node (plugin->views, node); + } + + return list; +} + +static uc_value_t * +uc_plymouth_mode (uc_vm_t *vm, size_t nargs) +{ + if (!plugin_instance) + return NULL; + + return ucv_string_new (mode_name (plugin_instance->mode)); +} + +static uc_value_t * +uc_plymouth_theme_dir (uc_vm_t *vm, size_t nargs) +{ + if (!plugin_instance || !plugin_instance->theme_dir) + return NULL; + + return ucv_string_new (plugin_instance->theme_dir); +} + +static uc_value_t * +uc_plymouth_elapsed (uc_vm_t *vm, size_t nargs) +{ + if (!plugin_instance) + return NULL; + + return ucv_double_new (now_seconds () - plugin_instance->started_at); +} + +static uc_value_t * +uc_plymouth_log (uc_vm_t *vm, size_t nargs) +{ + uc_value_t *msg = uc_fn_arg (0); + char *str; + + str = ucv_to_string (vm, msg); + + if (str) { + ply_trace ("ucode splash: %s", str); + free (str); + } + + return NULL; +} + +static const uc_function_list_t plymouth_functions[] = { + { "damage", uc_plymouth_damage }, + { "screens", uc_plymouth_screens }, + { "mode", uc_plymouth_mode }, + { "theme_dir", uc_plymouth_theme_dir }, + { "elapsed", uc_plymouth_elapsed }, + { "log", uc_plymouth_log } +}; + +static void +on_exception (uc_vm_t *vm, uc_exception_t *ex) +{ + ply_error ("ucode splash: %s", ex->message ? ex->message : "exception"); +} + +static void +on_ubus_event (struct ubus_context *ctx, struct ubus_event_handler *listener, + const char *type, struct blob_attr *msg) +{ + ply_boot_splash_plugin_t *plugin = plugin_instance; + uc_value_t *detail; + char *json; + + if (!plugin || !plugin->vm_ready) + return; + + json = msg ? blobmsg_format_json (msg, true) : NULL; + + detail = ucv_object_new (&plugin->vm); + ucv_object_add (detail, "type", ucv_string_new (type ? type : "")); + ucv_object_add (detail, "data", ucv_string_new (json ? json : "{}")); + + free (json); + + event_emit (plugin, "ubus", detail); +} + +static void +on_ubus_object (struct ubus_context *ctx, struct ubus_object_data *obj, + void *priv) +{ + ply_boot_splash_plugin_t *plugin = priv; + uc_value_t *detail; + + if (!plugin->vm_ready || !obj->path) + return; + + detail = ucv_object_new (&plugin->vm); + ucv_object_add (detail, "type", ucv_string_new ("ubus.object.add")); + ucv_object_add (detail, "data", + ucv_string_new_length (obj->path, strlen (obj->path))); + ucv_object_add (detail, "path", ucv_string_new (obj->path)); + ucv_object_add (detail, "present", ucv_boolean_new (true)); + + event_emit (plugin, "ubus", detail); +} + +static void +on_ubus_readable (void *user_data, int fd) +{ + ply_boot_splash_plugin_t *plugin = user_data; + + if (plugin->ubus) + ubus_handle_event (plugin->ubus); +} + +static bool +ubus_setup (ply_boot_splash_plugin_t *plugin) +{ + plugin->ubus = ubus_connect (NULL); + + if (!plugin->ubus) + return false; + + plugin->ubus_listener.cb = on_ubus_event; + ubus_register_event_handler (plugin->ubus, &plugin->ubus_listener, "*"); + + plugin->ubus_watch = ply_event_loop_watch_fd (plugin->loop, + plugin->ubus->sock.fd, + PLY_EVENT_LOOP_FD_STATUS_HAS_DATA, + on_ubus_readable, NULL, + plugin); + + event_emit (plugin, "ubus-connected", NULL); + + /* objects that registered before the splash connected are state, + * not events, and would otherwise never be seen */ + ubus_lookup (plugin->ubus, NULL, on_ubus_object, plugin); + + return true; +} + +static void +wait_stop (ply_boot_splash_plugin_t *plugin) +{ + if (plugin->wait_watch) { + ply_event_loop_stop_watching_fd (plugin->loop, + plugin->wait_watch); + plugin->wait_watch = NULL; + } + + if (plugin->wait_fd >= 0) { + close (plugin->wait_fd); + plugin->wait_fd = -1; + } +} + +/* ubusd creates the socket directory itself, well after the splash starts. + */ +static bool +wait_arm (ply_boot_splash_plugin_t *plugin) +{ + char path[PATH_MAX]; + char *slash; + + strncpy (path, UBUS_SOCKET_DIR, sizeof(path) - 1); + path[sizeof(path) - 1] = '\0'; + + while (inotify_add_watch (plugin->wait_fd, path, IN_CREATE) < 0) { + slash = strrchr (path, '/'); + + if (slash == NULL || slash == path) + return false; + + *slash = '\0'; + } + + return true; +} + +static void +on_ubus_appeared (void *user_data, int fd) +{ + ply_boot_splash_plugin_t *plugin = user_data; + char buf[4096]; + + while (read (fd, buf, sizeof(buf)) > 0) + ; + + wait_arm (plugin); + + if (ubus_setup (plugin)) + wait_stop (plugin); +} + +static void +ubus_wait (ply_boot_splash_plugin_t *plugin) +{ + plugin->wait_fd = inotify_init1 (IN_NONBLOCK | IN_CLOEXEC); + + if (plugin->wait_fd < 0) + return; + + if (!wait_arm (plugin)) { + close (plugin->wait_fd); + plugin->wait_fd = -1; + return; + } + + plugin->wait_watch = ply_event_loop_watch_fd (plugin->loop, + plugin->wait_fd, + PLY_EVENT_LOOP_FD_STATUS_HAS_DATA, + on_ubus_appeared, NULL, + plugin); +} + +static void +ubus_teardown (ply_boot_splash_plugin_t *plugin) +{ + wait_stop (plugin); + + if (plugin->ubus_watch) { + ply_event_loop_stop_watching_fd (plugin->loop, + plugin->ubus_watch); + plugin->ubus_watch = NULL; + } + + if (plugin->ubus) { + ubus_free (plugin->ubus); + plugin->ubus = NULL; + } +} + +static void +vm_teardown (ply_boot_splash_plugin_t *plugin) +{ + if (!plugin->vm_ready) + return; + + uc_vm_free (&plugin->vm); + plugin->vm_ready = false; + + if (plugin->program) { + uc_program_put (plugin->program); + plugin->program = NULL; + } + + if (plugin->source) { + uc_source_put (plugin->source); + plugin->source = NULL; + } + + uc_search_path_free (&plugin->config.module_search_path); +} + +static void +ucode_symbols_publish (void) +{ + static void *handle; + + if (handle) + return; + + /* plymouth dlopens plugins RTLD_LOCAL, so libucode arrives in a + * private scope and the modules the VM loads cannot relocate + * against it */ + handle = dlopen (UCODE_SONAME, RTLD_NOW | RTLD_GLOBAL); + + if (!handle) + ply_error ("ucode splash: %s: %s", UCODE_SONAME, dlerror ()); +} + +static bool +vm_setup (ply_boot_splash_plugin_t *plugin) +{ + uc_value_t *theme = NULL; + uc_value_t *scope; + char *err = NULL; + uc_value_t *mod; + + ucode_symbols_publish (); + + plugin->config = uc_default_parse_config; + /* the default is template mode, which would print the theme */ + plugin->config.raw_mode = true; + /* plymouthd owns the process signal disposition */ + plugin->config.setup_signal_handlers = false; + + /* the copied vector points at static storage and + * uc_search_path_init() appends rather than resets, so pushing + * onto it would realloc() memory that was never allocated */ + plugin->config.module_search_path.count = 0; + plugin->config.module_search_path.entries = NULL; + + uc_search_path_init (&plugin->config.module_search_path); + uc_search_path_add (&plugin->config.module_search_path, + "/usr/lib/ucode/*.so"); + uc_search_path_add (&plugin->config.module_search_path, + "/usr/share/ucode/*.uc"); + + plugin->source = uc_source_new_file (plugin->script_path); + + if (!plugin->source) { + ply_error ("ucode splash: cannot read %s", plugin->script_path); + uc_search_path_free (&plugin->config.module_search_path); + return false; + } + + plugin->program = uc_compile (&plugin->config, plugin->source, &err); + + if (!plugin->program) { + ply_error ("ucode splash: %s", err ? err : "compile failed"); + free (err); + uc_source_put (plugin->source); + plugin->source = NULL; + uc_search_path_free (&plugin->config.module_search_path); + return false; + } + + uc_vm_init (&plugin->vm, &plugin->config); + uc_vm_exception_handler_set (&plugin->vm, on_exception); + + scope = uc_vm_scope_get (&plugin->vm); + uc_stdlib_load (scope); + + mod = ucv_object_new (&plugin->vm); + uc_function_list_register (mod, plymouth_functions); + ucv_object_add (scope, "plymouth", mod); + + plugin->vm_ready = true; + + if (uc_vm_execute (&plugin->vm, plugin->program, &theme) != STATUS_OK) { + ply_error ("ucode splash: %s failed to run", + plugin->script_path); + ucv_put (theme); + vm_teardown (plugin); + return false; + } + + if (ucv_type (theme) != UC_OBJECT) { + ply_error ("ucode splash: %s returned no theme object", + plugin->script_path); + ucv_put (theme); + vm_teardown (plugin); + return false; + } + + uc_vm_registry_set (&plugin->vm, "plymouth.theme", theme); + + return true; +} + +static void +on_draw (void *user_data, ply_pixel_buffer_t *pixel_buffer, int x, int y, + int width, int height, ply_pixel_display_t *display) +{ + ply_boot_splash_plugin_t *plugin = user_data; + plutovg_surface_t *surface; + plutovg_canvas_t *canvas; + ply_rectangle_t painted; + ply_rectangle_t size; + uc_value_t *args[5]; + uint32_t *data; + + if (!plugin->vm_ready) + return; + + data = ply_pixel_buffer_get_argb32_data (pixel_buffer); + + if (!data) + return; + + ply_pixel_buffer_get_size (pixel_buffer, &size); + + surface = plutovg_surface_create_for_data ((unsigned char *)data, + size.width, size.height, + size.width * 4); + + if (!surface) + return; + + canvas = plutovg_canvas_create (surface); + plutovg_surface_destroy (surface); + + if (!canvas) + return; + + plutovg_canvas_rect (canvas, x, y, width, height); + plutovg_canvas_clip (canvas); + + args[0] = ucv_resource_create (&plugin->vm, "plutovg.canvas", canvas); + + if (!args[0]) { + plutovg_canvas_destroy (canvas); + return; + } + + args[1] = ucv_int64_new (x); + args[2] = ucv_int64_new (y); + args[3] = ucv_int64_new (width); + args[4] = ucv_int64_new (height); + + script_call (plugin, "paint", args, 5); + + /* drawing through the raw ARGB pointer bypasses the helpers that + * record what changed, so plymouth would upload nothing */ + painted.x = x; + painted.y = y; + painted.width = width; + painted.height = height; + ply_region_add_rectangle (ply_pixel_buffer_get_updated_areas (pixel_buffer), + &painted); +} + +static void +on_frame (void *user_data, ply_event_loop_t *loop) +{ + ply_boot_splash_plugin_t *plugin = user_data; + uc_value_t *args[1]; + + if (!plugin->animating) + return; + + args[0] = ucv_double_new (now_seconds () - plugin->started_at); + script_call (plugin, "frame", args, 1); + + ply_event_loop_watch_for_timeout (plugin->loop, plugin->frame_interval, + on_frame, plugin); +} + +static ply_boot_splash_plugin_t * +create_plugin (ply_key_file_t *key_file) +{ + ply_boot_splash_plugin_t *plugin; + double fps; + char *rate; + char *val; + + plugin = calloc (1, sizeof(*plugin)); + + if (!plugin) + return NULL; + + plugin->views = ply_list_new (); + plugin->wait_fd = -1; + plugin->frame_interval = 1.0 / DEFAULT_FRAME_RATE; + + val = ply_key_file_get_value (key_file, "ucode", "ScriptFile"); + plugin->script_path = val; + + val = ply_key_file_get_value (key_file, "ucode", "ThemeDir"); + plugin->theme_dir = val; + + rate = ply_key_file_get_value (key_file, "ucode", "FrameRate"); + + if (rate) { + fps = atof (rate); + + if (fps > 0.0) + plugin->frame_interval = 1.0 / fps; + + free (rate); + } + + plugin_instance = plugin; + + return plugin; +} + +static void +destroy_plugin (ply_boot_splash_plugin_t *plugin) +{ + ply_list_node_t *node; + view_t *view; + + if (!plugin) + return; + + vm_teardown (plugin); + + node = ply_list_get_first_node (plugin->views); + + while (node != NULL) { + view = ply_list_node_get_data (node); + free (view); + node = ply_list_get_next_node (plugin->views, node); + } + + ply_list_free (plugin->views); + + free (plugin->script_path); + free (plugin->theme_dir); + free (plugin); + + plugin_instance = NULL; +} + +static void +add_pixel_display (ply_boot_splash_plugin_t *plugin, + ply_pixel_display_t *display) +{ + view_t *view; + + view = calloc (1, sizeof(*view)); + + if (!view) + return; + + view->display = display; + ply_list_append_data (plugin->views, view); + + if (plugin->vm_ready) + ply_pixel_display_set_draw_handler (display, on_draw, plugin); +} + +static void +remove_pixel_display (ply_boot_splash_plugin_t *plugin, + ply_pixel_display_t *display) +{ + ply_list_node_t *node; + view_t *view; + + node = ply_list_get_first_node (plugin->views); + + while (node != NULL) { + view = ply_list_node_get_data (node); + + if (view->display == display) { + ply_pixel_display_set_draw_handler (display, NULL, + NULL); + ply_list_remove_node (plugin->views, node); + free (view); + return; + } + + node = ply_list_get_next_node (plugin->views, node); + } +} + +static void +add_text_display (ply_boot_splash_plugin_t *plugin, + ply_text_display_t *display) +{ +} + +static void +remove_text_display (ply_boot_splash_plugin_t *plugin, + ply_text_display_t *display) +{ +} + +static bool +show_splash_screen (ply_boot_splash_plugin_t *plugin, ply_event_loop_t *loop, + ply_buffer_t *boot_buffer, ply_boot_splash_mode_t mode) +{ + ply_list_node_t *node; + uc_value_t *args[1]; + view_t *view; + + plugin->loop = loop; + plugin->mode = mode; + plugin->started_at = now_seconds (); + + if (!plugin->script_path) { + ply_error ("ucode splash: theme sets no ScriptFile"); + return false; + } + + + if (!vm_setup (plugin)) + return false; + + node = ply_list_get_first_node (plugin->views); + + while (node != NULL) { + view = ply_list_node_get_data (node); + ply_pixel_display_set_draw_handler (view->display, on_draw, + plugin); + node = ply_list_get_next_node (plugin->views, node); + } + + args[0] = ucv_string_new (mode_name (mode)); + script_call (plugin, "setup", args, 1); + + plugin->animating = true; + ply_event_loop_watch_for_timeout (plugin->loop, plugin->frame_interval, + on_frame, plugin); + + if (!ubus_setup (plugin)) + ubus_wait (plugin); + + return true; +} + +static void +update_status (ply_boot_splash_plugin_t *plugin, const char *status) +{ + event_emit (plugin, "status", ucv_string_new (status ? status : "")); +} + +static void +on_boot_output (ply_boot_splash_plugin_t *plugin, const char *output, + size_t size) +{ + event_emit (plugin, "output", ucv_string_new_length (output, size)); +} + +static void +on_boot_progress (ply_boot_splash_plugin_t *plugin, double duration, + double fraction_done) +{ + uc_value_t *detail; + + detail = ucv_object_new (&plugin->vm); + ucv_object_add (detail, "duration", ucv_double_new (duration)); + ucv_object_add (detail, "fraction", ucv_double_new (fraction_done)); + + event_emit (plugin, "progress", detail); +} + +static void +on_root_mounted (ply_boot_splash_plugin_t *plugin) +{ + event_emit (plugin, "root-mounted", NULL); +} + +static void +system_update (ply_boot_splash_plugin_t *plugin, int progress) +{ + event_emit (plugin, "system-update", ucv_int64_new (progress)); +} + +static void +display_message (ply_boot_splash_plugin_t *plugin, const char *message) +{ + event_emit (plugin, "message", ucv_string_new (message ? message : "")); +} + +static void +hide_message (ply_boot_splash_plugin_t *plugin, const char *message) +{ + event_emit (plugin, "hide-message", + ucv_string_new (message ? message : "")); +} + +static void +display_normal (ply_boot_splash_plugin_t *plugin) +{ + event_emit (plugin, "normal", NULL); +} + +static void +display_password (ply_boot_splash_plugin_t *plugin, const char *prompt, + int bullets) +{ + event_emit (plugin, "password", ucv_int64_new (bullets)); +} + +static void +display_question (ply_boot_splash_plugin_t *plugin, const char *prompt, + const char *entry_text) +{ + event_emit (plugin, "question", ucv_string_new (prompt ? prompt : "")); +} + +static void +become_idle (ply_boot_splash_plugin_t *plugin, ply_trigger_t *idle_trigger) +{ + event_emit (plugin, "idle", NULL); + + ply_trigger_pull (idle_trigger, NULL); +} + +static void +hide_splash_screen (ply_boot_splash_plugin_t *plugin, ply_event_loop_t *loop) +{ + ply_list_node_t *node; + view_t *view; + + plugin->animating = false; + + if (plugin->loop) + ply_event_loop_stop_watching_for_timeout (plugin->loop, + on_frame, plugin); + + node = ply_list_get_first_node (plugin->views); + + while (node != NULL) { + view = ply_list_node_get_data (node); + ply_pixel_display_set_draw_handler (view->display, NULL, NULL); + node = ply_list_get_next_node (plugin->views, node); + } + + event_emit (plugin, "hide", NULL); + ubus_teardown (plugin); + vm_teardown (plugin); + + plugin->loop = NULL; +} + +ply_boot_splash_plugin_interface_t * +ply_boot_splash_plugin_get_interface (void) +{ + static ply_boot_splash_plugin_interface_t plugin_interface = { + .create_plugin = create_plugin, + .destroy_plugin = destroy_plugin, + .add_pixel_display = add_pixel_display, + .remove_pixel_display = remove_pixel_display, + .add_text_display = add_text_display, + .remove_text_display = remove_text_display, + .show_splash_screen = show_splash_screen, + .system_update = system_update, + .update_status = update_status, + .on_boot_output = on_boot_output, + .on_boot_progress = on_boot_progress, + .on_root_mounted = on_root_mounted, + .hide_splash_screen = hide_splash_screen, + .display_message = display_message, + .hide_message = hide_message, + .display_normal = display_normal, + .display_password = display_password, + .display_question = display_question, + .become_idle = become_idle + }; + + return &plugin_interface; +}