diff -ruN a/qapi/qom.json b/qapi/qom.json --- a/qapi/qom.json 2026-10-03 10:32:24 +++ b/qapi/qom.json 2026-10-03 10:32:24 @@ -544,6 +544,21 @@ '*height': 'str' } } ## +# @LambBoxShmProperties: +# +# Properties for lambbox-shm objects (LambBox shared-memory display). +# +# @device: id of the display device whose console is mirrored +# +# @path: file that is created and memory-mapped for the host app +# +# Since: 11.1 +## +{ 'struct': 'LambBoxShmProperties', + 'data': { 'device': 'str', + 'path': 'str' } } + +## # @InputLinuxProperties: # # Properties for input-linux objects. @@ -1283,6 +1298,7 @@ { 'name': 'igvm-cfg', 'if': 'CONFIG_IGVM' }, 'input-barrier', + 'lambbox-shm', { 'name': 'input-linux', 'if': 'CONFIG_LINUX' }, 'iommufd', @@ -1363,6 +1379,7 @@ 'igvm-cfg': { 'type': 'IgvmCfgProperties', 'if': 'CONFIG_IGVM' }, 'input-barrier': 'InputBarrierProperties', + 'lambbox-shm': 'LambBoxShmProperties', 'input-linux': { 'type': 'InputLinuxProperties', 'if': 'CONFIG_LINUX' }, 'iommufd': 'IOMMUFDProperties', diff -ruN a/ui/lambbox-shm.c b/ui/lambbox-shm.c --- a/ui/lambbox-shm.c 1970-01-01 09:00:00 +++ b/ui/lambbox-shm.c 2026-10-03 10:32:24 @@ -0,0 +1,310 @@ +/* + * LambBox shared-memory display + * + * Copies a console's framebuffer (and mouse cursor) into a memory-mapped + * file so the host app can draw it directly, without encoding it as VNC. + * + * -object lambbox-shm,id=shm0,device=gpu0,path=/path/to/fb + * + * File layout (little endian): + * 0 LBShmHeader (see below) + * 4096 cursor pixels, 256 x 256 x 4 bytes, RGBA (QEMUCursor format) + * 266240 framebuffer pixels, x8r8g8b8 (BGRA in memory), tightly packed + * (stride = width * 4), room for up to 7680 x 4320 + * + * The app polls header.frame / header.cursor_serial: when they change it + * redraws. The file is sized once at its maximum, so a resolution change + * never needs a new mapping. Readers may see a partly updated frame + * (tearing); the next frame fixes it. + * + * Copyright (c) 2026 LittleLamb Labs + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "qom/object_interfaces.h" +#include "qom/object.h" +#include "system/system.h" +#include "ui/console.h" +#include "ui/surface.h" +#include "ui/qemu-pixman.h" +#include + +#define TYPE_LAMBBOX_SHM "lambbox-shm" +OBJECT_DECLARE_SIMPLE_TYPE(LambBoxShm, LAMBBOX_SHM) + +#define LBSHM_MAGIC 0x4246424cu /* "LBFB" */ +#define LBSHM_VERSION 1 +#define LBSHM_CURSOR_OFF 4096 +#define LBSHM_CURSOR_MAX 256 +#define LBSHM_FB_OFF (LBSHM_CURSOR_OFF + LBSHM_CURSOR_MAX * LBSHM_CURSOR_MAX * 4) +#define LBSHM_FB_MAX_W 7680 +#define LBSHM_FB_MAX_H 4320 +#define LBSHM_SIZE (LBSHM_FB_OFF + (size_t)LBSHM_FB_MAX_W * LBSHM_FB_MAX_H * 4) + +typedef struct LBShmHeader { + uint32_t magic; + uint32_t version; + uint32_t width; /* 0 = no picture */ + uint32_t height; + uint64_t frame; /* bumped after each framebuffer update */ + uint32_t cursor_width; /* 0 = no cursor shape */ + uint32_t cursor_height; + int32_t cursor_hot_x; + int32_t cursor_hot_y; + uint64_t cursor_serial; /* bumped after each cursor shape change */ + int32_t mouse_x; + int32_t mouse_y; + uint32_t mouse_visible; + uint32_t reserved; +} LBShmHeader; + +struct LambBoxShm { + Object parent; + char *device; + char *path; + + int fd; + uint8_t *map; + LBShmHeader *header; + pixman_image_t *target; /* wraps the framebuffer area of the file */ + DisplaySurface *surface; + DisplayChangeListener dcl; + Notifier machine_done; + bool attached; +}; + +static void lbshm_publish_frame(LambBoxShm *s) +{ + smp_wmb(); + qatomic_set(&s->header->frame, s->header->frame + 1); +} + +static void lbshm_copy(LambBoxShm *s, int x, int y, int w, int h) +{ + if (!s->surface || !s->target) { + return; + } + pixman_image_composite(PIXMAN_OP_SRC, s->surface->image, NULL, s->target, + x, y, 0, 0, x, y, w, h); +} + +static void lbshm_gfx_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + LambBoxShm *s = container_of(dcl, LambBoxShm, dcl); + + lbshm_copy(s, x, y, w, h); + lbshm_publish_frame(s); +} + +static void lbshm_gfx_switch(DisplayChangeListener *dcl, + DisplaySurface *surface) +{ + LambBoxShm *s = container_of(dcl, LambBoxShm, dcl); + int width = surface ? surface_width(surface) : 0; + int height = surface ? surface_height(surface) : 0; + + s->surface = surface; + if (s->target) { + pixman_image_unref(s->target); + s->target = NULL; + } + if (width > LBSHM_FB_MAX_W || height > LBSHM_FB_MAX_H) { + width = height = 0; + } + if (width > 0 && height > 0) { + s->target = pixman_image_create_bits(PIXMAN_x8r8g8b8, width, height, + (uint32_t *)(s->map + LBSHM_FB_OFF), + width * 4); + } + s->header->width = width; + s->header->height = height; + lbshm_copy(s, 0, 0, width, height); + lbshm_publish_frame(s); +} + +static void lbshm_refresh(DisplayChangeListener *dcl) +{ + qemu_console_hw_update(dcl->con); +} + +static bool lbshm_check_format(DisplayChangeListener *dcl, + pixman_format_code_t format) +{ + /* pixman converts anything into the file's x8r8g8b8 */ + return true; +} + +static void lbshm_mouse_set(DisplayChangeListener *dcl, int x, int y, bool on) +{ + LambBoxShm *s = container_of(dcl, LambBoxShm, dcl); + + s->header->mouse_x = x; + s->header->mouse_y = y; + s->header->mouse_visible = on; +} + +static void lbshm_cursor_define(DisplayChangeListener *dcl, QEMUCursor *c) +{ + LambBoxShm *s = container_of(dcl, LambBoxShm, dcl); + uint32_t *dst = (uint32_t *)(s->map + LBSHM_CURSOR_OFF); + int w = c ? MIN(c->width, LBSHM_CURSOR_MAX) : 0; + int h = c ? MIN(c->height, LBSHM_CURSOR_MAX) : 0; + int row; + + for (row = 0; row < h; row++) { + memcpy(dst + row * w, c->data + row * c->width, w * 4); + } + s->header->cursor_width = w; + s->header->cursor_height = h; + s->header->cursor_hot_x = c ? c->hot_x : 0; + s->header->cursor_hot_y = c ? c->hot_y : 0; + smp_wmb(); + qatomic_set(&s->header->cursor_serial, s->header->cursor_serial + 1); +} + +static const DisplayChangeListenerOps lbshm_ops = { + .dpy_name = "lambbox-shm", + .dpy_refresh = lbshm_refresh, + .dpy_gfx_update = lbshm_gfx_update, + .dpy_gfx_switch = lbshm_gfx_switch, + .dpy_gfx_check_format = lbshm_check_format, + .dpy_mouse_set = lbshm_mouse_set, + .dpy_cursor_define = lbshm_cursor_define, +}; + +/* Devices (and their consoles) exist only after machine init */ +static void lbshm_machine_done(Notifier *notifier, void *data) +{ + LambBoxShm *s = container_of(notifier, LambBoxShm, machine_done); + QemuConsole *con; + Error *err = NULL; + + con = qemu_console_lookup_by_device_name(s->device, 0, &err); + if (!con) { + error_report_err(err); + return; + } + qemu_console_register_listener(con, &s->dcl, &lbshm_ops); + /* about 60 times a second (default is 30) */ + qemu_console_listener_set_refresh(&s->dcl, 16); + s->attached = true; +} + +static void lbshm_complete(UserCreatable *uc, Error **errp) +{ + LambBoxShm *s = LAMBBOX_SHM(uc); + + if (!s->device || !s->path) { + error_setg(errp, "lambbox-shm needs 'device' and 'path'"); + return; + } + s->fd = open(s->path, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (s->fd < 0) { + error_setg_errno(errp, errno, "lambbox-shm: cannot create %s", s->path); + return; + } + if (ftruncate(s->fd, LBSHM_SIZE) < 0) { + error_setg_errno(errp, errno, "lambbox-shm: cannot size %s", s->path); + close(s->fd); + s->fd = -1; + return; + } + s->map = mmap(NULL, LBSHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, 0); + if (s->map == MAP_FAILED) { + error_setg_errno(errp, errno, "lambbox-shm: cannot map %s", s->path); + s->map = NULL; + close(s->fd); + s->fd = -1; + return; + } + s->header = (LBShmHeader *)s->map; + memset(s->header, 0, sizeof(*s->header)); + s->header->magic = LBSHM_MAGIC; + s->header->version = LBSHM_VERSION; + + s->machine_done.notify = lbshm_machine_done; + qemu_add_machine_init_done_notifier(&s->machine_done); +} + +static char *lbshm_get_device(Object *obj, Error **errp) +{ + return g_strdup(LAMBBOX_SHM(obj)->device); +} + +static void lbshm_set_device(Object *obj, const char *value, Error **errp) +{ + LambBoxShm *s = LAMBBOX_SHM(obj); + + g_free(s->device); + s->device = g_strdup(value); +} + +static char *lbshm_get_path(Object *obj, Error **errp) +{ + return g_strdup(LAMBBOX_SHM(obj)->path); +} + +static void lbshm_set_path(Object *obj, const char *value, Error **errp) +{ + LambBoxShm *s = LAMBBOX_SHM(obj); + + g_free(s->path); + s->path = g_strdup(value); +} + +static void lbshm_instance_init(Object *obj) +{ + LAMBBOX_SHM(obj)->fd = -1; +} + +static void lbshm_instance_finalize(Object *obj) +{ + LambBoxShm *s = LAMBBOX_SHM(obj); + + if (s->attached) { + qemu_console_unregister_listener(&s->dcl); + } + if (s->target) { + pixman_image_unref(s->target); + } + if (s->map) { + munmap(s->map, LBSHM_SIZE); + } + if (s->fd >= 0) { + close(s->fd); + } + g_free(s->device); + g_free(s->path); +} + +static void lbshm_class_init(ObjectClass *oc, const void *data) +{ + UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc); + + ucc->complete = lbshm_complete; + object_class_property_add_str(oc, "device", lbshm_get_device, lbshm_set_device); + object_class_property_add_str(oc, "path", lbshm_get_path, lbshm_set_path); +} + +static const TypeInfo lbshm_info = { + .name = TYPE_LAMBBOX_SHM, + .parent = TYPE_OBJECT, + .class_init = lbshm_class_init, + .instance_size = sizeof(LambBoxShm), + .instance_init = lbshm_instance_init, + .instance_finalize = lbshm_instance_finalize, + .interfaces = (const InterfaceInfo[]) { + { TYPE_USER_CREATABLE }, + { } + } +}; + +static void lbshm_register_types(void) +{ + type_register_static(&lbshm_info); +} + +type_init(lbshm_register_types); diff -ruN a/ui/meson.build b/ui/meson.build --- a/ui/meson.build 2026-10-03 10:32:24 +++ b/ui/meson.build 2026-10-03 10:32:24 @@ -55,6 +55,7 @@ system_ss.add(png) system_ss.add(files( 'input-barrier.c', + 'lambbox-shm.c', 'input.c', 'ui-hmp-cmds.c', 'ui-qmp-cmds.c',