From d5027ac53778d529ec4e3d0c405766d47b037207 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Thu, 5 Mar 2026 14:23:22 +0200 Subject: [PATCH 01/14] Add support for Wayland on Linux The Wayland support comes from changing the way input and output works: 1. Screen output is captured via DRM (Direct Rendering Manager) 2. Keyboard/Mouse input is injected via libevdev Both of these methods are lower level than the previous X11 code, so they're actually capable of becoming full replacements for the X11 mechanisms, should we ever want to do that. I've tried to make the changes minimally invasive to the existing code, instead of a big refactor into X11 vs DRM code paths. Known issues: 1. Logging out back to the greeter breaks the screen scraping. This is fixable, just requires work to figure some things out. 2. Cursor state is not reflected in the screen output To get the greeter working will require some more surgery. I haven't figured out how all of that session stuff works yet, but it is possible. It's largely just an issue of the system getting confused between Wayland/X11 states. One quite simple solution could be that once we detect Wayland, we remain in that state for the duration of the process life. I'm not sure if people care about dynamic switching between X11 and Wayland during a single boot - and I suspect not, so I think this would be a simple and robust solution to get the greeter working. If cursor state is necessary, then I can look into that. It just hasn't felt like a priority. I don't consider this work 100% finished. We still need to get the greeter working, and more testing/feedback. But if possible, I'd like to get this on a public branch so that I can start getting feedback. This work is sponsored by touchsource.com --- makefile | 11 +- meshcore/KVM/Linux/linux_events.c | 26 +- meshcore/KVM/Linux/linux_events.h | 7 + meshcore/KVM/Linux/linux_events_evdev.c | 914 +++++++++++++++ meshcore/KVM/Linux/linux_kvm.c | 40 +- meshcore/KVM/Linux/linux_kvm_drm.c | 1402 +++++++++++++++++++++++ meshcore/KVM/Linux/linux_kvm_drm.h | 28 + meshcore/KVM/Linux/linux_kvm_drm_egl.c | 549 +++++++++ meshcore/KVM/Linux/linux_kvm_drm_egl.h | 78 ++ meshcore/KVM/Linux/linux_kvm_wayland.c | 95 ++ meshcore/KVM/Linux/linux_kvm_wayland.h | 30 + meshcore/agentcore.c | 27 +- 12 files changed, 3183 insertions(+), 24 deletions(-) create mode 100644 meshcore/KVM/Linux/linux_events_evdev.c create mode 100644 meshcore/KVM/Linux/linux_kvm_drm.c create mode 100644 meshcore/KVM/Linux/linux_kvm_drm.h create mode 100644 meshcore/KVM/Linux/linux_kvm_drm_egl.c create mode 100644 meshcore/KVM/Linux/linux_kvm_drm_egl.h create mode 100644 meshcore/KVM/Linux/linux_kvm_wayland.c create mode 100644 meshcore/KVM/Linux/linux_kvm_wayland.h diff --git a/makefile b/makefile index f845dafd6..00aaa8f0f 100644 --- a/makefile +++ b/makefile @@ -541,9 +541,14 @@ endif ifeq ($(KVM),1) # Mesh Agent KVM, this is only included in builds that have KVM support -LINUXKVMSOURCES = meshcore/KVM/Linux/linux_kvm.c meshcore/KVM/Linux/linux_events.c meshcore/KVM/Linux/linux_tile.c meshcore/KVM/Linux/linux_compression.c +LINUXKVMSOURCES = meshcore/KVM/Linux/linux_kvm.c meshcore/KVM/Linux/linux_kvm_wayland.c meshcore/KVM/Linux/linux_kvm_drm.c meshcore/KVM/Linux/linux_kvm_drm_egl.c meshcore/KVM/Linux/linux_events.c meshcore/KVM/Linux/linux_events_evdev.c meshcore/KVM/Linux/linux_tile.c meshcore/KVM/Linux/linux_compression.c MACOSKVMSOURCES = meshcore/KVM/MacOS/mac_kvm.c meshcore/KVM/MacOS/mac_events.c meshcore/KVM/MacOS/mac_tile.c meshcore/KVM/Linux/linux_compression.c CFLAGS += -D_LINKVM + DRMCFLAGS = $(shell pkg-config --cflags libdrm egl glesv2 2>/dev/null) + DRMLIBS = $(shell pkg-config --libs libdrm egl glesv2 2>/dev/null) + ifneq ($(strip $(DRMCFLAGS)),) + CFLAGS += $(DRMCFLAGS) + endif ifneq ($(JPEGVER),) ifeq ($(LEGACY_LD),1) LINUXFLAGS = lib-jpeg-turbo/linux/$(ARCHNAME)/$(JPEGVER)/libturbojpeg.a @@ -785,11 +790,11 @@ $(LIBNAME): $(OBJECTS) $(SOURCES) # Compile on Raspberry Pi 2/3 with KVM pi: - $(MAKE) EXENAME="meshagent_pi" CFLAGS="-std=gnu99 -g -Wall -D_POSIX -DMICROSTACK_PROXY -DMICROSTACK_TLS_DETECT -D_LINKVM $(CWEBLOG) $(CWATCHDOG) -fno-strict-aliasing $(INCDIRS) -DMESH_AGENTID=25 -D_NOFSWATCHER -D_NOHECI" ADDITIONALSOURCES="$(LINUXKVMSOURCES)" LDFLAGS="-Lopenssl/libstatic/linux/pi -lrt $(LINUXSSL) $(LINUXFLAGS) $(LDFLAGS) $(LDEXTRA) -ldl" + $(MAKE) EXENAME="meshagent_pi" CFLAGS="-std=gnu99 -g -Wall -D_POSIX -DMICROSTACK_PROXY -DMICROSTACK_TLS_DETECT -D_LINKVM $(CWEBLOG) $(CWATCHDOG) -fno-strict-aliasing $(INCDIRS) -DMESH_AGENTID=25 -D_NOFSWATCHER -D_NOHECI" ADDITIONALSOURCES="$(LINUXKVMSOURCES)" LDFLAGS="-Lopenssl/libstatic/linux/pi -lrt $(LINUXSSL) $(LINUXFLAGS) $(LDFLAGS) $(LDEXTRA) $(DRMLIBS) -ldl" strip meshagent_pi linux: - $(MAKE) EXENAME="$(EXENAME)_$(ARCHNAME)$(EXENAME2)" AID="$(ARCHID)" ADDITIONALSOURCES="$(LINUXKVMSOURCES)" ADDITIONALFLAGS="-lrt -z noexecstack -z relro -z now" CFLAGS="-DJPEGMAXBUF=$(KVMMaxTile) -DMESH_AGENTID=$(ARCHID) $(CFLAGS) $(CEXTRA)" LDFLAGS="$(LINUXSSL) $(LINUXFLAGS) $(LDFLAGS) $(LDEXTRA) -ldl" + $(MAKE) EXENAME="$(EXENAME)_$(ARCHNAME)$(EXENAME2)" AID="$(ARCHID)" ADDITIONALSOURCES="$(LINUXKVMSOURCES)" ADDITIONALFLAGS="-lrt -z noexecstack -z relro -z now $(DRMLIBS)" CFLAGS="-DJPEGMAXBUF=$(KVMMaxTile) -DMESH_AGENTID=$(ARCHID) $(CFLAGS) $(CEXTRA)" LDFLAGS="$(LINUXSSL) $(LINUXFLAGS) $(LDFLAGS) $(LDEXTRA) -ldl" $(SYMBOLCP) $(STRIP) diff --git a/meshcore/KVM/Linux/linux_events.c b/meshcore/KVM/Linux/linux_events.c index 4e2f35c22..18223f766 100644 --- a/meshcore/KVM/Linux/linux_events.c +++ b/meshcore/KVM/Linux/linux_events.c @@ -137,6 +137,12 @@ static struct keymap_t g_keymap[] = { void MouseAction(double absX, double absY, int button, short wheel, Display *display) { + if (kvm_events_evdev_is_active()) + { + kvm_events_evdev_mouse_action(absX, absY, button, wheel); + return; + } + if (change_display) { return; } @@ -204,6 +210,11 @@ void KeyAction(unsigned char vk, int up, Display *display) unsigned int keycode = 0; if (up == 4) { up = 0; } + if (kvm_events_evdev_is_active()) + { + kvm_events_evdev_key_action(vk, up); + return; + } if (up && (vk == 0x14 || vk == 0x90 || vk == 0x91)) { @@ -265,6 +276,12 @@ void KeyAction(unsigned char vk, int up, Display *display) } void KeyActionUnicode_UNMAP_ALL(Display *display) { + if (kvm_events_evdev_is_active()) + { + UNREFERENCED_PARAMETER(display); + return; + } + int i; for (i = 0; i < g_keyboardMapCount; ++i) { @@ -278,6 +295,13 @@ void KeyActionUnicode_UNMAP_ALL(Display *display) } void KeyActionUnicode(uint16_t unicode, int up, Display *display) { + if (kvm_events_evdev_is_active()) + { + UNREFERENCED_PARAMETER(display); + kvm_events_evdev_key_action_unicode(unicode, up); + return; + } + if (change_display) { return; } if (up == 0) @@ -313,4 +337,4 @@ void KeyActionUnicode(uint16_t unicode, int up, Display *display) } } } -} \ No newline at end of file +} diff --git a/meshcore/KVM/Linux/linux_events.h b/meshcore/KVM/Linux/linux_events.h index bcb0c414b..46b458991 100644 --- a/meshcore/KVM/Linux/linux_events.h +++ b/meshcore/KVM/Linux/linux_events.h @@ -436,6 +436,13 @@ struct keymap_t { unsigned char vk; }; +extern int kvm_events_evdev_init(); +extern void kvm_events_evdev_shutdown(); +extern int kvm_events_evdev_is_active(); +extern void kvm_events_evdev_mouse_action(double absX, double absY, int button, short wheel); +extern void kvm_events_evdev_key_action(unsigned char vk, int up); +extern void kvm_events_evdev_key_action_unicode(uint16_t unicode, int up); + extern void MouseAction(double absX, double absY, int button, short wheel, Display *display); extern void KeyAction(unsigned char vk, int up, Display *display); extern void KeyActionUnicode(uint16_t unicode, int up, Display *display); diff --git a/meshcore/KVM/Linux/linux_events_evdev.c b/meshcore/KVM/Linux/linux_events_evdev.c new file mode 100644 index 000000000..0d0ba73f8 --- /dev/null +++ b/meshcore/KVM/Linux/linux_events_evdev.c @@ -0,0 +1,914 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "linux_events.h" + +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) + +#include + +struct libevdev; +struct libevdev_uinput; + +typedef struct libevdev *(*kvm_libevdev_new_fn)(void); +typedef void (*kvm_libevdev_free_fn)(struct libevdev *dev); +typedef int (*kvm_libevdev_set_name_fn)(struct libevdev *dev, const char *name); +typedef int (*kvm_libevdev_enable_event_type_fn)(struct libevdev *dev, unsigned int type); +typedef int (*kvm_libevdev_enable_event_code_fn)(struct libevdev *dev, unsigned int type, unsigned int code, const void *data); +typedef int (*kvm_libevdev_uinput_create_from_device_fn)(const struct libevdev *dev, int uinput_fd, struct libevdev_uinput **uinput_dev); +typedef void (*kvm_libevdev_uinput_destroy_fn)(struct libevdev_uinput *uinput_dev); +typedef int (*kvm_libevdev_uinput_write_event_fn)(const struct libevdev_uinput *uinput_dev, unsigned int type, unsigned int code, int value); +typedef const char *(*kvm_libevdev_strerror_fn)(int errcode); + +typedef struct kvm_evdev_exports +{ + void *library; + kvm_libevdev_new_fn libevdev_new; + kvm_libevdev_free_fn libevdev_free; + kvm_libevdev_set_name_fn libevdev_set_name; + kvm_libevdev_enable_event_type_fn libevdev_enable_event_type; + kvm_libevdev_enable_event_code_fn libevdev_enable_event_code; + kvm_libevdev_uinput_create_from_device_fn libevdev_uinput_create_from_device; + kvm_libevdev_uinput_destroy_fn libevdev_uinput_destroy; + kvm_libevdev_uinput_write_event_fn libevdev_uinput_write_event; + kvm_libevdev_strerror_fn libevdev_strerror; +} kvm_evdev_exports; + +typedef struct kvm_evdev_state +{ + struct libevdev *dev; + struct libevdev_uinput *uinput; + int active; +} kvm_evdev_state; + +static kvm_evdev_exports g_kvm_evdev_exports = {0}; +static kvm_evdev_state g_kvm_evdev_state = {0}; + +extern int SCREEN_WIDTH; +extern int SCREEN_HEIGHT; + +static const unsigned int g_kvm_evdev_alpha_keycodes[26] = { + KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, + KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z +}; +static const unsigned int g_kvm_evdev_digit_keycodes[10] = { + KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9 +}; +static const unsigned int g_kvm_evdev_numpad_keycodes[10] = { + KEY_KP0, KEY_KP1, KEY_KP2, KEY_KP3, KEY_KP4, KEY_KP5, KEY_KP6, KEY_KP7, KEY_KP8, KEY_KP9 +}; + +#define KVM_LIBEVDEV_UINPUT_OPEN_MANAGED -2 +#ifndef O_CLOEXEC +#define O_CLOEXEC 0 +#endif + +#ifndef KEY_BOOKMARKS +#define KEY_BOOKMARKS KEY_RESERVED +#endif +#ifndef KEY_HOMEPAGE +#define KEY_HOMEPAGE KEY_RESERVED +#endif +#ifndef KEY_MEDIA +#define KEY_MEDIA KEY_RESERVED +#endif +#ifndef KEY_PROG1 +#define KEY_PROG1 KEY_RESERVED +#endif +#ifndef KEY_PROG2 +#define KEY_PROG2 KEY_RESERVED +#endif +#ifndef KEY_BREAK +#define KEY_BREAK KEY_RESERVED +#endif +#ifndef KEY_OK +#define KEY_OK KEY_RESERVED +#endif +#ifndef KEY_KPCOMMA +#define KEY_KPCOMMA KEY_RESERVED +#endif +#ifndef KEY_PRINT +#define KEY_PRINT KEY_RESERVED +#endif + +void kvm_events_evdev_shutdown(); + +static int kvm_events_evdev_load_exports() +{ + if (g_kvm_evdev_exports.library != NULL) + { + return 1; + } + + g_kvm_evdev_exports.library = dlopen("libevdev.so.2", RTLD_NOW | RTLD_LOCAL); + if (g_kvm_evdev_exports.library == NULL) + { + g_kvm_evdev_exports.library = dlopen("libevdev.so", RTLD_NOW | RTLD_LOCAL); + if (g_kvm_evdev_exports.library == NULL) + { + return 0; + } + } + +#define KVM_DLSYM_REQ(name) \ + g_kvm_evdev_exports.name = (kvm_##name##_fn)dlsym(g_kvm_evdev_exports.library, #name); \ + if (g_kvm_evdev_exports.name == NULL) \ + { \ + dlclose(g_kvm_evdev_exports.library); \ + memset(&g_kvm_evdev_exports, 0, sizeof(g_kvm_evdev_exports)); \ + return 0; \ + } + + KVM_DLSYM_REQ(libevdev_new); + KVM_DLSYM_REQ(libevdev_free); + KVM_DLSYM_REQ(libevdev_set_name); + KVM_DLSYM_REQ(libevdev_enable_event_type); + KVM_DLSYM_REQ(libevdev_enable_event_code); + KVM_DLSYM_REQ(libevdev_uinput_create_from_device); + KVM_DLSYM_REQ(libevdev_uinput_destroy); + KVM_DLSYM_REQ(libevdev_uinput_write_event); + g_kvm_evdev_exports.libevdev_strerror = (kvm_libevdev_strerror_fn)dlsym(g_kvm_evdev_exports.library, "libevdev_strerror"); + +#undef KVM_DLSYM_REQ + return 1; +} + +static const char *kvm_events_evdev_error_string(int rc) +{ + int e = rc < 0 ? -rc : rc; + const char *ret = NULL; + + if (g_kvm_evdev_exports.libevdev_strerror != NULL) + { + ret = g_kvm_evdev_exports.libevdev_strerror(e); + } + if (ret == NULL) + { + ret = strerror(e); + } + return ret == NULL ? "unknown error" : ret; +} + +static int kvm_events_evdev_try_open_uinput() +{ + static const char *paths[] = { "/dev/uinput", "/dev/input/uinput", "/dev/misc/uinput" }; + size_t i = 0; + int fd = -1; + int lastErr = ENOENT; + + for (i = 0; i < (sizeof(paths) / sizeof(paths[0])); ++i) + { + fd = open(paths[i], O_RDWR | O_CLOEXEC); + if (fd >= 0) + { + return fd; + } + lastErr = errno; + } + errno = lastErr; + return -1; +} + +static unsigned int kvm_events_evdev_vk_to_keycode(unsigned char vk) +{ + if (vk >= VK_0 && vk <= VK_9) + { + return g_kvm_evdev_digit_keycodes[vk - VK_0]; + } + if (vk >= VK_A && vk <= VK_Z) + { + return g_kvm_evdev_alpha_keycodes[vk - VK_A]; + } + if (vk >= VK_NUMPAD0 && vk <= VK_NUMPAD9) + { + return g_kvm_evdev_numpad_keycodes[vk - VK_NUMPAD0]; + } + if (vk >= VK_F1 && vk <= VK_F24) + { + return KEY_F1 + (vk - VK_F1); + } + + switch (vk) + { + case VK_BACK: + return KEY_BACKSPACE; + case VK_TAB: + return KEY_TAB; + case VK_CLEAR: + return KEY_CLEAR; + case VK_RETURN: + return KEY_ENTER; + case VK_SHIFT: + return KEY_LEFTSHIFT; + case VK_CONTROL: + return KEY_LEFTCTRL; + case VK_MENU: + return KEY_LEFTALT; + case VK_PAUSE: + return KEY_PAUSE; + case VK_CAPITAL: + return KEY_CAPSLOCK; + case VK_ESCAPE: + return KEY_ESC; + case VK_SPACE: + return KEY_SPACE; + case VK_PRIOR: + return KEY_PAGEUP; + case VK_NEXT: + return KEY_PAGEDOWN; + case VK_END: + return KEY_END; + case VK_HOME: + return KEY_HOME; + case VK_LEFT: + return KEY_LEFT; + case VK_UP: + return KEY_UP; + case VK_RIGHT: + return KEY_RIGHT; + case VK_DOWN: + return KEY_DOWN; + case VK_SELECT: + return KEY_SELECT; + case VK_PRINT: + return KEY_PRINT; + case VK_EXECUTE: + return KEY_OK; + case VK_SNAPSHOT: + return KEY_SYSRQ; + case VK_INSERT: + return KEY_INSERT; + case VK_DELETE: + return KEY_DELETE; + case VK_HELP: + return KEY_HELP; + case VK_CANCEL: + return KEY_BREAK; + case VK_LWIN: + return KEY_LEFTMETA; + case VK_RWIN: + return KEY_RIGHTMETA; + case VK_APPS: + return KEY_MENU; + case VK_SLEEP: + return KEY_SLEEP; + case VK_MULTIPLY: + return KEY_KPASTERISK; + case VK_ADD: + return KEY_KPPLUS; + case VK_SEPARATOR: + return KEY_KPCOMMA; + case VK_SUBTRACT: + return KEY_KPMINUS; + case VK_DECIMAL: + return KEY_KPDOT; + case VK_DIVIDE: + return KEY_KPSLASH; + case VK_NUMLOCK: + return KEY_NUMLOCK; + case VK_SCROLL: + return KEY_SCROLLLOCK; + case VK_LSHIFT: + return KEY_LEFTSHIFT; + case VK_RSHIFT: + return KEY_RIGHTSHIFT; + case VK_LCONTROL: + return KEY_LEFTCTRL; + case VK_RCONTROL: + return KEY_RIGHTCTRL; + case VK_LMENU: + return KEY_LEFTALT; + case VK_RMENU: + return KEY_RIGHTALT; + case VK_BROWSER_BACK: + return KEY_BACK; + case VK_BROWSER_FORWARD: + return KEY_FORWARD; + case VK_BROWSER_REFRESH: + return KEY_REFRESH; + case VK_BROWSER_STOP: + return KEY_STOP; + case VK_BROWSER_SEARCH: + return KEY_SEARCH; + case VK_BROWSER_FAVORITES: + return KEY_BOOKMARKS; + case VK_BROWSER_HOME: + return KEY_HOMEPAGE; + case VK_VOLUME_MUTE: + return KEY_MUTE; + case VK_VOLUME_DOWN: + return KEY_VOLUMEDOWN; + case VK_VOLUME_UP: + return KEY_VOLUMEUP; + case VK_MEDIA_NEXT_TRACK: + return KEY_NEXTSONG; + case VK_MEDIA_PREV_TRACK: + return KEY_PREVIOUSSONG; + case VK_MEDIA_STOP: + return KEY_STOPCD; + case VK_MEDIA_PLAY_PAUSE: + return KEY_PLAYPAUSE; + case VK_MEDIA_LAUNCH_MAIL: + return KEY_MAIL; + case VK_MEDIA_LAUNCH_MEDIA_SELECT: + return KEY_MEDIA; + case VK_MEDIA_LAUNCH_APP1: + return KEY_PROG1; + case VK_MEDIA_LAUNCH_APP2: + return KEY_PROG2; + case VK_OEM_1: + return KEY_SEMICOLON; + case VK_OEM_PLUS: + return KEY_EQUAL; + case VK_OEM_COMMA: + return KEY_COMMA; + case VK_OEM_MINUS: + return KEY_MINUS; + case VK_OEM_PERIOD: + return KEY_DOT; + case VK_OEM_2: + return KEY_SLASH; + case VK_OEM_3: + return KEY_GRAVE; + case VK_OEM_4: + return KEY_LEFTBRACE; + case VK_OEM_5: + return KEY_BACKSLASH; + case VK_OEM_6: + return KEY_RIGHTBRACE; + case VK_OEM_7: + return KEY_APOSTROPHE; + default: + return KEY_RESERVED; + } +} + +static int kvm_events_evdev_ascii_to_keycode(uint16_t unicode, unsigned int *keycode, int *needsShift) +{ + *needsShift = 0; + *keycode = KEY_RESERVED; + + if (unicode >= 'a' && unicode <= 'z') + { + *keycode = g_kvm_evdev_alpha_keycodes[unicode - 'a']; + return 1; + } + if (unicode >= 'A' && unicode <= 'Z') + { + *keycode = g_kvm_evdev_alpha_keycodes[unicode - 'A']; + *needsShift = 1; + return 1; + } + if (unicode >= '0' && unicode <= '9') + { + *keycode = g_kvm_evdev_digit_keycodes[unicode - '0']; + return 1; + } + + switch (unicode) + { + case ' ': + *keycode = KEY_SPACE; + return 1; + case '\t': + *keycode = KEY_TAB; + return 1; + case '\n': + case '\r': + *keycode = KEY_ENTER; + return 1; + case '\b': + *keycode = KEY_BACKSPACE; + return 1; + case '-': + *keycode = KEY_MINUS; + return 1; + case '_': + *keycode = KEY_MINUS; + *needsShift = 1; + return 1; + case '=': + *keycode = KEY_EQUAL; + return 1; + case '+': + *keycode = KEY_EQUAL; + *needsShift = 1; + return 1; + case '[': + *keycode = KEY_LEFTBRACE; + return 1; + case '{': + *keycode = KEY_LEFTBRACE; + *needsShift = 1; + return 1; + case ']': + *keycode = KEY_RIGHTBRACE; + return 1; + case '}': + *keycode = KEY_RIGHTBRACE; + *needsShift = 1; + return 1; + case '\\': + *keycode = KEY_BACKSLASH; + return 1; + case '|': + *keycode = KEY_BACKSLASH; + *needsShift = 1; + return 1; + case ';': + *keycode = KEY_SEMICOLON; + return 1; + case ':': + *keycode = KEY_SEMICOLON; + *needsShift = 1; + return 1; + case '\'': + *keycode = KEY_APOSTROPHE; + return 1; + case '"': + *keycode = KEY_APOSTROPHE; + *needsShift = 1; + return 1; + case ',': + *keycode = KEY_COMMA; + return 1; + case '<': + *keycode = KEY_COMMA; + *needsShift = 1; + return 1; + case '.': + *keycode = KEY_DOT; + return 1; + case '>': + *keycode = KEY_DOT; + *needsShift = 1; + return 1; + case '/': + *keycode = KEY_SLASH; + return 1; + case '?': + *keycode = KEY_SLASH; + *needsShift = 1; + return 1; + case '`': + *keycode = KEY_GRAVE; + return 1; + case '~': + *keycode = KEY_GRAVE; + *needsShift = 1; + return 1; + case '!': + *keycode = KEY_1; + *needsShift = 1; + return 1; + case '@': + *keycode = KEY_2; + *needsShift = 1; + return 1; + case '#': + *keycode = KEY_3; + *needsShift = 1; + return 1; + case '$': + *keycode = KEY_4; + *needsShift = 1; + return 1; + case '%': + *keycode = KEY_5; + *needsShift = 1; + return 1; + case '^': + *keycode = KEY_6; + *needsShift = 1; + return 1; + case '&': + *keycode = KEY_7; + *needsShift = 1; + return 1; + case '*': + *keycode = KEY_8; + *needsShift = 1; + return 1; + case '(': + *keycode = KEY_9; + *needsShift = 1; + return 1; + case ')': + *keycode = KEY_0; + *needsShift = 1; + return 1; + default: + return 0; + } +} + +static int kvm_events_evdev_write(unsigned int type, unsigned int code, int value) +{ + int r = 0; + if (g_kvm_evdev_state.active == 0 || g_kvm_evdev_state.uinput == NULL) + { + return -1; + } + + r = g_kvm_evdev_exports.libevdev_uinput_write_event(g_kvm_evdev_state.uinput, type, code, value); + if (r != 0) + { + kvm_events_evdev_shutdown(); + } + return r; +} + +static int kvm_events_evdev_sync() +{ + return kvm_events_evdev_write(EV_SYN, SYN_REPORT, 0); +} + +static int kvm_events_evdev_scale_axis(int value, int maxPixels) +{ + if (maxPixels <= 1) + { + if (value < 0) + { + return 0; + } + if (value > 65535) + { + return 65535; + } + return value; + } + + if (value < 0) + { + value = 0; + } + if (value >= maxPixels) + { + value = maxPixels - 1; + } + return (int)(((uint64_t)value * 65535ULL) / ((uint64_t)(maxPixels - 1))); +} + +int kvm_events_evdev_init() +{ + struct input_absinfo absInfo; + unsigned char vk = 0; + int keyEnabled[KEY_MAX + 1]; + unsigned int keycode = KEY_RESERVED; + int r = 0; + int createRc = 0; + int createManagedRc = 0; + int uinputFd = -1; + int uinputErrno = 0; + + if (g_kvm_evdev_state.active != 0) + { + return 1; + } + if (!kvm_events_evdev_load_exports()) + { + printf("MeshAgent: Failed to load libevdev symbols\n"); + return 0; + } + + if (g_kvm_evdev_state.dev != NULL || g_kvm_evdev_state.uinput != NULL) + { + kvm_events_evdev_shutdown(); + } + memset(&g_kvm_evdev_state, 0, sizeof(g_kvm_evdev_state)); + + g_kvm_evdev_state.dev = g_kvm_evdev_exports.libevdev_new(); + if (g_kvm_evdev_state.dev == NULL) + { + return 0; + } + + ignore_result(g_kvm_evdev_exports.libevdev_set_name(g_kvm_evdev_state.dev, "MeshAgent Virtual Input")); + + r = g_kvm_evdev_exports.libevdev_enable_event_type(g_kvm_evdev_state.dev, EV_KEY); + if (r != 0) + { + printf("MeshAgent: libevdev_enable_event_type(EV_KEY) failed: %d (%s)\n", r, kvm_events_evdev_error_string(r)); + goto error; + } + r = g_kvm_evdev_exports.libevdev_enable_event_type(g_kvm_evdev_state.dev, EV_REL); + if (r != 0) + { + printf("MeshAgent: libevdev_enable_event_type(EV_REL) failed: %d (%s)\n", r, kvm_events_evdev_error_string(r)); + goto error; + } + r = g_kvm_evdev_exports.libevdev_enable_event_type(g_kvm_evdev_state.dev, EV_ABS); + if (r != 0) + { + printf("MeshAgent: libevdev_enable_event_type(EV_ABS) failed: %d (%s)\n", r, kvm_events_evdev_error_string(r)); + goto error; + } + + memset(&absInfo, 0, sizeof(absInfo)); + absInfo.minimum = 0; + absInfo.maximum = 65535; + r = g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_ABS, ABS_X, &absInfo); + if (r != 0) + { + printf("MeshAgent: libevdev_enable_event_code(EV_ABS, ABS_X) failed: %d (%s)\n", r, kvm_events_evdev_error_string(r)); + goto error; + } + r = g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_ABS, ABS_Y, &absInfo); + if (r != 0) + { + printf("MeshAgent: libevdev_enable_event_code(EV_ABS, ABS_Y) failed: %d (%s)\n", r, kvm_events_evdev_error_string(r)); + goto error; + } + + r = g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_REL, REL_WHEEL, NULL); + if (r != 0) + { + printf("MeshAgent: libevdev_enable_event_code(EV_REL, REL_WHEEL) failed: %d (%s)\n", r, kvm_events_evdev_error_string(r)); + goto error; + } + ignore_result(g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_REL, REL_HWHEEL, NULL)); + + memset(keyEnabled, 0, sizeof(keyEnabled)); + keyEnabled[BTN_LEFT] = g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, BTN_LEFT, NULL) == 0; + keyEnabled[BTN_RIGHT] = g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, BTN_RIGHT, NULL) == 0; + keyEnabled[BTN_MIDDLE] = g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, BTN_MIDDLE, NULL) == 0; + ignore_result(g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, BTN_SIDE, NULL)); + ignore_result(g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, BTN_EXTRA, NULL)); + + for (vk = 0; vk < 0xFF; ++vk) + { + keycode = kvm_events_evdev_vk_to_keycode(vk); + if (keycode != KEY_RESERVED && keycode <= KEY_MAX && !keyEnabled[keycode]) + { + if (g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, keycode, NULL) == 0) + { + keyEnabled[keycode] = 1; + } + } + } + keycode = kvm_events_evdev_vk_to_keycode((unsigned char)0xFF); + if (keycode != KEY_RESERVED && keycode <= KEY_MAX && !keyEnabled[keycode]) + { + ignore_result(g_kvm_evdev_exports.libevdev_enable_event_code(g_kvm_evdev_state.dev, EV_KEY, keycode, NULL)); + } + + createManagedRc = g_kvm_evdev_exports.libevdev_uinput_create_from_device(g_kvm_evdev_state.dev, KVM_LIBEVDEV_UINPUT_OPEN_MANAGED, &g_kvm_evdev_state.uinput); + createRc = createManagedRc; + if (createRc != 0) + { + uinputFd = kvm_events_evdev_try_open_uinput(); + uinputErrno = errno; + if (uinputFd >= 0) + { + createRc = g_kvm_evdev_exports.libevdev_uinput_create_from_device(g_kvm_evdev_state.dev, uinputFd, &g_kvm_evdev_state.uinput); + if (createRc != 0) + { + close(uinputFd); + uinputFd = -1; + } + } + if (createRc != 0) + { + int rwAccess = access("/dev/uinput", R_OK | W_OK); + printf("MeshAgent: libevdev_uinput_create_from_device failed (managed=%d:%s, fallback=%d:%s, /dev/uinput access=%d errno=%d:%s, open_errno=%d:%s, uid=%d euid=%d)\n", + createManagedRc, kvm_events_evdev_error_string(createManagedRc), + createRc, kvm_events_evdev_error_string(createRc), + rwAccess, errno, strerror(errno), + uinputErrno, strerror(uinputErrno), + (int)getuid(), (int)geteuid()); + goto error; + } + } + + printf("MeshAgent: evdev virtual input device created successfully\n"); + usleep(50000); + g_kvm_evdev_state.active = 1; + return 1; + +error: + printf("MeshAgent: Failed to create evdev virtual input device\n"); + kvm_events_evdev_shutdown(); + return 0; +} + +void kvm_events_evdev_shutdown() +{ + if (g_kvm_evdev_state.uinput != NULL) + { + g_kvm_evdev_exports.libevdev_uinput_destroy(g_kvm_evdev_state.uinput); + g_kvm_evdev_state.uinput = NULL; + } + if (g_kvm_evdev_state.dev != NULL) + { + g_kvm_evdev_exports.libevdev_free(g_kvm_evdev_state.dev); + g_kvm_evdev_state.dev = NULL; + } + g_kvm_evdev_state.active = 0; +} + +int kvm_events_evdev_is_active() +{ + return g_kvm_evdev_state.active; +} + +void kvm_events_evdev_mouse_action(double absX, double absY, int button, short wheel) +{ + int x = (int)absX; + int y = (int)absY; + unsigned int mouseCode = 0; + int mouseValue = 0; + + if (!kvm_events_evdev_is_active()) + { + return; + } + if (button == 0x88) + { + return; + } + + if (kvm_events_evdev_write(EV_ABS, ABS_X, kvm_events_evdev_scale_axis(x, SCREEN_WIDTH)) != 0) + { + return; + } + if (kvm_events_evdev_write(EV_ABS, ABS_Y, kvm_events_evdev_scale_axis(y, SCREEN_HEIGHT)) != 0) + { + return; + } + + if (button != 0) + { + switch (button) + { + case MOUSEEVENTF_LEFTDOWN: + mouseCode = BTN_LEFT; + mouseValue = 1; + break; + case MOUSEEVENTF_LEFTUP: + mouseCode = BTN_LEFT; + mouseValue = 0; + break; + case MOUSEEVENTF_RIGHTDOWN: + mouseCode = BTN_RIGHT; + mouseValue = 1; + break; + case MOUSEEVENTF_RIGHTUP: + mouseCode = BTN_RIGHT; + mouseValue = 0; + break; + case MOUSEEVENTF_MIDDLEDOWN: + mouseCode = BTN_MIDDLE; + mouseValue = 1; + break; + case MOUSEEVENTF_MIDDLEUP: + mouseCode = BTN_MIDDLE; + mouseValue = 0; + break; + default: + mouseCode = 0; + break; + } + if (mouseCode != 0) + { + if (kvm_events_evdev_write(EV_KEY, mouseCode, mouseValue) != 0) + { + return; + } + } + } + else if (wheel != 0) + { + if (kvm_events_evdev_write(EV_REL, REL_WHEEL, wheel > 0 ? 1 : -1) != 0) + { + return; + } + } + + ignore_result(kvm_events_evdev_sync()); +} + +void kvm_events_evdev_key_action(unsigned char vk, int up) +{ + unsigned int keycode = kvm_events_evdev_vk_to_keycode(vk); + int value = up == 0 ? 1 : 0; + + if (!kvm_events_evdev_is_active()) + { + return; + } + if (up == 4) + { + value = 1; + } + if (keycode == KEY_RESERVED) + { + return; + } + + if (kvm_events_evdev_write(EV_KEY, keycode, value) != 0) + { + return; + } + ignore_result(kvm_events_evdev_sync()); +} + +void kvm_events_evdev_key_action_unicode(uint16_t unicode, int up) +{ + unsigned int keycode = KEY_RESERVED; + int needsShift = 0; + + if (!kvm_events_evdev_is_active()) + { + return; + } + if (up != 0) + { + return; + } + if (!kvm_events_evdev_ascii_to_keycode(unicode, &keycode, &needsShift)) + { + return; + } + + if (needsShift) + { + if (kvm_events_evdev_write(EV_KEY, KEY_LEFTSHIFT, 1) != 0) + { + return; + } + } + if (kvm_events_evdev_write(EV_KEY, keycode, 1) != 0) + { + return; + } + if (kvm_events_evdev_write(EV_KEY, keycode, 0) != 0) + { + return; + } + if (needsShift) + { + if (kvm_events_evdev_write(EV_KEY, KEY_LEFTSHIFT, 0) != 0) + { + return; + } + } + ignore_result(kvm_events_evdev_sync()); +} + +#else + +int kvm_events_evdev_init() +{ + return 0; +} + +void kvm_events_evdev_shutdown() +{ +} + +int kvm_events_evdev_is_active() +{ + return 0; +} + +void kvm_events_evdev_mouse_action(double absX, double absY, int button, short wheel) +{ + UNREFERENCED_PARAMETER(absX); + UNREFERENCED_PARAMETER(absY); + UNREFERENCED_PARAMETER(button); + UNREFERENCED_PARAMETER(wheel); +} + +void kvm_events_evdev_key_action(unsigned char vk, int up) +{ + UNREFERENCED_PARAMETER(vk); + UNREFERENCED_PARAMETER(up); +} + +void kvm_events_evdev_key_action_unicode(uint16_t unicode, int up) +{ + UNREFERENCED_PARAMETER(unicode); + UNREFERENCED_PARAMETER(up); +} + +#endif diff --git a/meshcore/KVM/Linux/linux_kvm.c b/meshcore/KVM/Linux/linux_kvm.c index 131c31fdf..847952a0e 100644 --- a/meshcore/KVM/Linux/linux_kvm.c +++ b/meshcore/KVM/Linux/linux_kvm.c @@ -15,6 +15,8 @@ limitations under the License. */ #include "linux_kvm.h" +#include "linux_kvm_wayland.h" +#include "linux_kvm_drm.h" #include "meshcore/meshdefines.h" #include "microstack/ILibParsers.h" #include "microstack/ILibAsyncSocket.h" @@ -22,6 +24,7 @@ limitations under the License. #include "microstack/ILibProcessPipe.h" #include #include +#include #include #include @@ -447,6 +450,13 @@ int lockfileCheckFn(const struct dirent *ent) { void getAvailableDisplays(unsigned short **array, int *len) { int i; + if (g_kvmBackendDRM != 0) + { + *len = 1; + if ((*array = (unsigned short*)malloc(sizeof(unsigned short))) == NULL) ILIBCRITICALEXIT(254); + (*array)[0] = 0; + return; + } *len = x11_exports->XScreenCount(eventdisplay); if ((*array = (unsigned short *)malloc((*len) * sizeof(unsigned short))) == NULL) ILIBCRITICALEXIT(254); for (i = 0; i < (*len); ++i) @@ -648,6 +658,9 @@ int kvm_init(int displayNo) void CheckDesktopSwitch(int checkres) { + UNREFERENCED_PARAMETER(checkres); + if (g_kvmBackendDRM != 0) { return; } + if (change_display) { if (logFile) { fprintf(logFile, "kvm_init(%d) checkDesktopSwitch\n", CURRENT_DISPLAY_ID); fflush(logFile); } @@ -768,6 +781,12 @@ int kvm_server_inputdata(char* block, int blocklen) } case MNG_KVM_SET_DISPLAY: { + if (g_kvmBackendDRM != 0) + { + CURRENT_DISPLAY_ID = 0; + change_display = 0; + break; + } if (ntohs(((unsigned short*)(block))[2]) == CURRENT_DISPLAY_ID) { break; } // Don't do anything CURRENT_DISPLAY_ID = ntohs(((unsigned short*)(block))[2]); change_display = 1; @@ -948,8 +967,10 @@ void kvm_server_sighandler(int signum, siginfo_t *info, void *context) { g_shutdown = 1; } -void* kvm_server_mainloop(void* parm) + +void* kvm_server_mainloop_x11(void* parm) { + int sessionUid = (int)(intptr_t)parm; int maxsleep; Window rr, cr; int rx, ry, wx, wy, rs; @@ -980,6 +1001,8 @@ void* kvm_server_mainloop(void* parm) unsigned short currentDisplayId = 0; + if (sessionUid != 0) { ignore_result(setuid((uid_t)sessionUid)); } + if (logFile) { fprintf(logFile, "Checking $DISPLAY\n"); fflush(logFile); } for (char **env = environ; *env; ++env) { @@ -1357,6 +1380,18 @@ void* kvm_server_mainloop(void* parm) return (void*)0; } +void* kvm_server_mainloop(void* parm) +{ + int sessionUid = (int)(intptr_t)parm; + kvm_screenreader_mode_t screenreaderMode = kvm_screenreader_mode_for_uid(sessionUid); + if (screenreaderMode == KVM_SCREENREADER_MODE_DRM) + { + if (logFile) { fprintf(logFile, "Using DRM KVM backend\n"); fflush(logFile); } + return kvm_server_mainloop_drm(parm); + } + return kvm_server_mainloop_x11(parm); +} + void kvm_relay_readSink(ILibProcessPipe_Pipe sender, char *buffer, size_t bufferLen, size_t* bytesConsumed) { ILibKVM_WriteHandler writeHandler = (ILibKVM_WriteHandler)((void**)ILibMemory_Extra(sender))[0]; @@ -1463,7 +1498,6 @@ void* kvm_relay_restart(int paused, void *processPipeMgr, ILibKVM_WriteHandler w close(master2slave[1]); if (SLAVELOG != 0) { logFile = fopen("/tmp/slave", "w"); } - if (uid != 0) { ignore_result(setuid(uid)); } if (g_ILibCrashDump_path != NULL) { @@ -1482,7 +1516,7 @@ void* kvm_relay_restart(int paused, void *processPipeMgr, ILibKVM_WriteHandler w if (authToken != NULL) { setenv("XAUTHORITY", authToken, 1); } if (dispid != NULL) { setenv("DISPLAY", dispid, 1); } - kvm_server_mainloop((void*)0); + kvm_server_mainloop((void*)(intptr_t)uid); exit(0); return(NULL); } diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c new file mode 100644 index 000000000..c8884c92c --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -0,0 +1,1402 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "linux_kvm_drm.h" +#include "linux_kvm_drm_egl.h" +#include "linux_kvm.h" +#include "linux_compression.h" +#include "linux_tile.h" +#include "meshcore/meshdefines.h" +#include "microstack/ILibParsers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef O_CLOEXEC +#define KVM_DRM_O_CLOEXEC 0 +#else +#define KVM_DRM_O_CLOEXEC O_CLOEXEC +#endif +#endif + +#define KVM_DRM_MAX_ERROR 256 + +int g_kvmBackendDRM = 0; +static int drm_debug = 1; + +extern int SCREEN_NUM; +extern int SCREEN_WIDTH; +extern int SCREEN_HEIGHT; +extern int SCREEN_DEPTH; +extern int TILE_WIDTH; +extern int TILE_HEIGHT; +extern int TILE_WIDTH_COUNT; +extern int TILE_HEIGHT_COUNT; +extern int COMPRESSION_RATIO; +extern int FRAME_RATE_TIMER; +extern struct tileInfo_t **g_tileInfo; +extern int g_remotepause; +extern int g_pause; +extern int g_shutdown; +extern int master2slave[2]; +extern int slave2master[2]; +extern int CURRENT_DISPLAY_ID; +extern int g_enableEvents; +extern void *tilebuffer; +extern unsigned char *jpeg_buffer; +extern int jpeg_buffer_length; + +extern void kvm_send_error(char *msg); +extern void kvm_send_resolution(); +extern void kvm_send_display(); +extern void kvm_send_display_list(); +extern int kvm_server_inputdata(char *block, int blocklen); +extern void kvm_server_sighandler(int signum, siginfo_t *info, void *context); + +static int kvm_drm_write_all(int fd, const char *buffer, size_t len) +{ + size_t offset = 0; + ssize_t written = 0; + + while (offset < len) + { + written = write(fd, buffer + offset, len - offset); + if (written < 0) + { + if (errno == EINTR) + { + continue; + } + return -1; + } + if (written == 0) + { + return -1; + } + offset += (size_t)written; + } + return 0; +} + +static int kvm_drm_send_dirty_tiles(const unsigned char *rgbBuffer, size_t rgbSize, char **desktopBuffer, long long *desktopBufferSize) +{ + int r = 0; + int c = 0; + int x = 0; + int y = 0; + int width = 0; + int height = 0; + int paddedWidth = 0; + int paddedHeight = 0; + size_t rowBytes = 0; + size_t rowPaddedBytes = 0; + size_t requiredSize = 0; + + if (SCREEN_WIDTH <= 0 || SCREEN_HEIGHT <= 0 || TILE_WIDTH_COUNT <= 0 || TILE_HEIGHT_COUNT <= 0) + { + return 0; + } + + rowBytes = ((size_t)SCREEN_WIDTH) * 3u; + requiredSize = ((size_t)SCREEN_WIDTH) * ((size_t)SCREEN_HEIGHT) * 3u; + if (rgbBuffer == NULL || rgbSize < requiredSize) + { + return -1; + } + + paddedWidth = adjust_screen_size(SCREEN_WIDTH); + paddedHeight = adjust_screen_size(SCREEN_HEIGHT); + rowPaddedBytes = ((size_t)paddedWidth) * 3u; + requiredSize = ((size_t)paddedWidth) * ((size_t)paddedHeight) * 3u; + + if (*desktopBufferSize != (long long)requiredSize) + { + char *tmp = NULL; + if (*desktopBuffer != NULL) + { + free(*desktopBuffer); + *desktopBuffer = NULL; + } + tmp = (char *)malloc(requiredSize); + if (tmp == NULL) ILIBCRITICALEXIT(254); + *desktopBuffer = tmp; + *desktopBufferSize = (long long)requiredSize; + } + + for (y = 0; y < SCREEN_HEIGHT; ++y) + { + char *dst = *desktopBuffer + (((size_t)y) * rowPaddedBytes); + const char *src = (const char *)rgbBuffer + (((size_t)y) * rowBytes); + memcpy_s(dst, rowPaddedBytes, src, rowBytes); + if (rowPaddedBytes > rowBytes) + { + memset(dst + rowBytes, 0, rowPaddedBytes - rowBytes); + } + } + + if (paddedHeight > SCREEN_HEIGHT) + { + char *dst = *desktopBuffer + (((size_t)SCREEN_HEIGHT) * rowPaddedBytes); + size_t bytes = ((size_t)(paddedHeight - SCREEN_HEIGHT)) * rowPaddedBytes; + memset(dst, 0, bytes); + } + + for (r = 0; r < TILE_HEIGHT_COUNT; ++r) + { + for (c = 0; c < TILE_WIDTH_COUNT; ++c) + { + g_tileInfo[r][c].flag = TILE_TODO; +#ifdef KVM_ALL_TILES + g_tileInfo[r][c].crc = 0xFF; +#endif + } + } + + for (y = 0; y < TILE_HEIGHT_COUNT; ++y) + { + for (x = 0; x < TILE_WIDTH_COUNT; ++x) + { + void *tilePacket = NULL; + long long tilePacketSize = 0; + + height = TILE_HEIGHT * y; + width = TILE_WIDTH * x; + + if (g_tileInfo[y][x].flag == TILE_SENT || g_tileInfo[y][x].flag == TILE_DONT_SEND) + { + continue; + } + + getTileAt(width, height, &tilePacket, &tilePacketSize, *desktopBuffer, *desktopBufferSize, y, x); + if (tilePacket != NULL && tilePacketSize > 0) + { + if (kvm_drm_write_all(slave2master[1], (char *)tilePacket, (size_t)tilePacketSize) != 0) + { + free(tilePacket); + return -1; + } + free(tilePacket); + } + } + } + + return 0; +} + +#if defined(__linux__) + +typedef struct kvm_drm_output +{ + char device_path[64]; + char connector_name[32]; + uint32_t connector_id; + uint32_t crtc_id; + int crtc_index; +} kvm_drm_output; + +typedef struct kvm_drm_frame_map +{ + uint32_t handle; + uint8_t *addr; + size_t size; + int dma_fd; +} kvm_drm_frame_map; + +typedef struct kvm_drm_scanout_frame +{ + uint32_t fb_id; + uint32_t width; + uint32_t height; + uint32_t pitch; + uint32_t offset; + uint32_t format; + uint32_t handle; + uint64_t modifier; +} kvm_drm_scanout_frame; + +static void kvm_drm_copy_error_message(char *dst, size_t dst_size, const char *src) +{ + if (dst == NULL || dst_size == 0) + { + return; + } + if (src == NULL) + { + dst[0] = 0; + return; + } + + size_t n = strnlen_s(src, dst_size - 1); + memcpy_s(dst, dst_size, src, n); + dst[n] = 0; +} + +static const char *kvm_drm_connector_type_name(uint32_t t) +{ + switch (t) + { + case DRM_MODE_CONNECTOR_Unknown: + return "Unknown"; + case DRM_MODE_CONNECTOR_VGA: + return "VGA"; + case DRM_MODE_CONNECTOR_DVII: + return "DVI-I"; + case DRM_MODE_CONNECTOR_DVID: + return "DVI-D"; + case DRM_MODE_CONNECTOR_DVIA: + return "DVI-A"; + case DRM_MODE_CONNECTOR_Composite: + return "Composite"; + case DRM_MODE_CONNECTOR_SVIDEO: + return "SVIDEO"; + case DRM_MODE_CONNECTOR_LVDS: + return "LVDS"; + case DRM_MODE_CONNECTOR_Component: + return "Component"; + case DRM_MODE_CONNECTOR_9PinDIN: + return "DIN"; + case DRM_MODE_CONNECTOR_DisplayPort: + return "DP"; + case DRM_MODE_CONNECTOR_HDMIA: + return "HDMI-A"; + case DRM_MODE_CONNECTOR_HDMIB: + return "HDMI-B"; + case DRM_MODE_CONNECTOR_TV: + return "TV"; + case DRM_MODE_CONNECTOR_eDP: + return "eDP"; + case DRM_MODE_CONNECTOR_VIRTUAL: + return "Virtual"; + case DRM_MODE_CONNECTOR_DSI: + return "DSI"; + default: + return "Connector"; + } +} + +static void kvm_drm_destroy_map(kvm_drm_frame_map *map) +{ + if (map->addr != NULL && map->size > 0) + { + munmap(map->addr, map->size); + } + if (map->dma_fd >= 0) + { + close(map->dma_fd); + } + map->handle = 0; + map->addr = NULL; + map->size = 0; + map->dma_fd = -1; +} + +static bool kvm_drm_map_framebuffer_handle(int fd, uint32_t handle, size_t min_size, kvm_drm_frame_map *map, char *out_error, size_t out_error_size) +{ + if (map->handle == handle && map->addr != NULL && map->size >= min_size) + { + return true; + } + + kvm_drm_destroy_map(map); + + struct drm_mode_map_dumb map_dumb; + memset(&map_dumb, 0, sizeof(map_dumb)); + map_dumb.handle = handle; + if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb) == 0) + { + void *ptr = mmap(NULL, min_size, PROT_READ, MAP_SHARED, fd, map_dumb.offset); + if (ptr != MAP_FAILED) + { + map->handle = handle; + map->addr = (uint8_t *)ptr; + map->size = min_size; + return true; + } + } + + int dma_fd = -1; + if (drmPrimeHandleToFD(fd, handle, DRM_CLOEXEC | DRM_RDWR, &dma_fd) == 0) + { + void *ptr = mmap(NULL, min_size, PROT_READ, MAP_SHARED, dma_fd, 0); + if (ptr != MAP_FAILED) + { + map->handle = handle; + map->addr = (uint8_t *)ptr; + map->size = min_size; + map->dma_fd = dma_fd; + return true; + } + close(dma_fd); + } + + kvm_drm_copy_error_message(out_error, out_error_size, + "Failed to map scanout buffer (requires mappable dumb/linear buffer and DRM access)"); + return false; +} + +static uint32_t kvm_drm_pick_crtc_for_connector(int fd, const drmModeRes *res, const drmModeConnector *conn) +{ + uint32_t crtc_id = 0; + int i; + if (conn->encoder_id != 0) + { + drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoder_id); + if (enc != NULL) + { + crtc_id = enc->crtc_id; + drmModeFreeEncoder(enc); + if (crtc_id != 0) + { + for (i = 0; i < res->count_crtcs; ++i) + { + if (res->crtcs[i] == crtc_id) + { + return crtc_id; + } + } + } + } + } + + for (i = 0; i < conn->count_encoders; ++i) + { + drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoders[i]); + if (enc == NULL) + { + continue; + } + crtc_id = enc->crtc_id; + drmModeFreeEncoder(enc); + if (crtc_id != 0) + { + int j; + for (j = 0; j < res->count_crtcs; ++j) + { + if (res->crtcs[j] == crtc_id) + { + return crtc_id; + } + } + } + } + + return 0; +} + +static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_output *out, char *out_error, size_t out_error_size) +{ + drmModeRes *res = drmModeGetResources(fd); + if (res == NULL) + { + kvm_drm_copy_error_message(out_error, out_error_size, "drmModeGetResources failed"); + return false; + } + + bool found = false; + int i; + for (i = 0; i < res->count_connectors; ++i) + { + drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[i]); + if (conn == NULL) + { + continue; + } + + if (conn->connection != DRM_MODE_CONNECTED || conn->count_modes <= 0) + { + drmModeFreeConnector(conn); + continue; + } + + uint32_t crtc_id = kvm_drm_pick_crtc_for_connector(fd, res, conn); + if (crtc_id == 0) + { + drmModeFreeConnector(conn); + continue; + } + + int c; + int crtc_index = -1; + for (c = 0; c < res->count_crtcs; ++c) + { + if (res->crtcs[c] == crtc_id) + { + crtc_index = c; + break; + } + } + if (crtc_index < 0) + { + drmModeFreeConnector(conn); + continue; + } + + snprintf(out->device_path, sizeof(out->device_path), "%s", path); + snprintf(out->connector_name, sizeof(out->connector_name), "%s-%u", kvm_drm_connector_type_name(conn->connector_type), conn->connector_type_id); + out->connector_id = conn->connector_id; + out->crtc_id = crtc_id; + out->crtc_index = crtc_index; + + found = true; + drmModeFreeConnector(conn); + break; + } + + drmModeFreeResources(res); + + if (!found) + { + char err[KVM_DRM_MAX_ERROR]; + snprintf(err, sizeof(err), "No connected display with active CRTC on %s", path); + kvm_drm_copy_error_message(out_error, out_error_size, err); + } + return found; +} + +static bool kvm_drm_open_device_with_output(const char *explicit_device, int *out_fd, kvm_drm_output *out, char *out_error, size_t out_error_size) +{ + int i; + if (explicit_device != NULL && explicit_device[0] != 0) + { + int fd = open(explicit_device, O_RDWR | KVM_DRM_O_CLOEXEC | O_NONBLOCK); + if (fd < 0) + { + char err[KVM_DRM_MAX_ERROR]; + snprintf(err, sizeof(err), "Unable to open DRM device: %s", explicit_device); + kvm_drm_copy_error_message(out_error, out_error_size, err); + return false; + } + if (kvm_drm_find_active_output_on_fd(fd, explicit_device, out, out_error, out_error_size)) + { + *out_fd = fd; + return true; + } + close(fd); + return false; + } + + for (i = 0; i < 16; ++i) + { + char path[64]; + snprintf(path, sizeof(path), "/dev/dri/card%d", i); + int fd = open(path, O_RDWR | KVM_DRM_O_CLOEXEC | O_NONBLOCK); + if (fd < 0) + { + continue; + } + if (kvm_drm_find_active_output_on_fd(fd, path, out, out_error, out_error_size)) + { + *out_fd = fd; + return true; + } + close(fd); + } + + kvm_drm_copy_error_message(out_error, out_error_size, "No usable /dev/dri/card* device with an active connector/CRTC"); + return false; +} + +// We only read scanout state; holding DRM master can block the compositor from +// taking GPU ownership during greeter/user-session handoff. +static int kvm_drm_drop_master_if_held(int fd) +{ + if (fd < 0) { return 0; } + if (drmDropMaster(fd) == 0) { return 0; } + + // Not master, or driver doesn't implement master semantics for this fd. + if (errno == EINVAL || errno == ENOTTY || errno == ENOSYS) + { + return 0; + } + return -1; +} + +static uint32_t kvm_drm_get_plane_fb_id(int fd, uint32_t crtc_id, int crtc_index) +{ + drmModePlaneRes *pres = drmModeGetPlaneResources(fd); + if (pres == NULL) + { + return 0; + } + + uint32_t best_fb_id = 0; + uint64_t best_area = 0; + uint32_t i; + + for (i = 0; i < pres->count_planes; ++i) + { + drmModePlane *plane = drmModeGetPlane(fd, pres->planes[i]); + if (plane == NULL) + { + continue; + } + if (plane->crtc_id == crtc_id && plane->fb_id != 0 && + (plane->possible_crtcs & (1u << (uint32_t)crtc_index))) + { + drmModeFB *fb = drmModeGetFB(fd, plane->fb_id); + if (fb != NULL) + { + uint64_t area = ((uint64_t)fb->width) * ((uint64_t)fb->height); + if (area > best_area) + { + best_area = area; + best_fb_id = plane->fb_id; + } + drmModeFreeFB(fb); + } + } + drmModeFreePlane(plane); + } + + drmModeFreePlaneResources(pres); + return best_fb_id; +} + +static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, kvm_drm_scanout_frame *out, char *out_error, size_t out_error_size) +{ + drmModeCrtc *crtc = drmModeGetCrtc(fd, crtc_id); + if (crtc == NULL) + { + kvm_drm_copy_error_message(out_error, out_error_size, "drmModeGetCrtc failed"); + return false; + } + + uint32_t fb_id = crtc->buffer_id; + drmModeFreeCrtc(crtc); + + if (fb_id == 0) + { + fb_id = kvm_drm_get_plane_fb_id(fd, crtc_id, crtc_index); + } + if (fb_id == 0) + { + kvm_drm_copy_error_message(out_error, out_error_size, "Active CRTC has no framebuffer"); + return false; + } + + bool have_fb2_meta = false; + drmModeFB2 *fb2 = drmModeGetFB2(fd, fb_id); + if (fb2 != NULL) + { + if (fb2->handles[1] == 0 && fb2->handles[2] == 0 && fb2->handles[3] == 0) + { + out->width = fb2->width; + out->height = fb2->height; + out->pitch = fb2->pitches[0]; + out->offset = fb2->offsets[0]; + out->format = fb2->pixel_format; + out->modifier = fb2->modifier; + if (fb2->handles[0] != 0) + { + out->fb_id = fb_id; + out->handle = fb2->handles[0]; + drmModeFreeFB2(fb2); + return true; + } + have_fb2_meta = true; + } + drmModeFreeFB2(fb2); + } + + drmModeFB *fb = drmModeGetFB(fd, fb_id); + if (fb == NULL) + { + kvm_drm_copy_error_message(out_error, out_error_size, "drmModeGetFB failed"); + return false; + } + + out->fb_id = fb_id; + if (!have_fb2_meta) + { + out->width = fb->width; + out->height = fb->height; + out->pitch = fb->pitch; + out->offset = 0; + out->format = DRM_FORMAT_XRGB8888; + out->modifier = DRM_FORMAT_MOD_LINEAR; + } + out->handle = fb->handle; + drmModeFreeFB(fb); + return true; +} + +static uint32_t kvm_drm_bytes_per_pixel(uint32_t format) +{ + switch (format) + { + case DRM_FORMAT_XRGB8888: + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_XBGR8888: + case DRM_FORMAT_ABGR8888: + case DRM_FORMAT_RGBX8888: + case DRM_FORMAT_RGBA8888: + case DRM_FORMAT_BGRX8888: + case DRM_FORMAT_BGRA8888: + case DRM_FORMAT_XRGB2101010: + case DRM_FORMAT_XBGR2101010: + case DRM_FORMAT_ARGB2101010: + case DRM_FORMAT_ABGR2101010: + case DRM_FORMAT_RGBX1010102: + case DRM_FORMAT_BGRX1010102: + case DRM_FORMAT_RGBA1010102: + case DRM_FORMAT_BGRA1010102: + return 4; + case DRM_FORMAT_RGB888: + case DRM_FORMAT_BGR888: + return 3; + case DRM_FORMAT_RGB565: + case DRM_FORMAT_BGR565: + return 2; + default: + return 0; + } +} + +static bool kvm_drm_convert_to_rgb24(const kvm_drm_scanout_frame *f, const uint8_t *src, uint8_t *rgb, size_t rgb_capacity, size_t *rgb_size_out, char *out_error, size_t out_error_size) +{ + static uint8_t expand5[32]; + static uint8_t expand6[64]; + static int expandTablesReady = 0; + uint32_t i; + + if (rgb_size_out != NULL) + { + *rgb_size_out = 0; + } + if (f->modifier != DRM_FORMAT_MOD_INVALID && f->modifier != DRM_FORMAT_MOD_LINEAR) + { + kvm_drm_copy_error_message(out_error, out_error_size, "Non-linear DRM modifier is not supported by CPU readback path"); + return false; + } + + uint32_t bpp = kvm_drm_bytes_per_pixel(f->format); + if (bpp == 0) + { + char err[KVM_DRM_MAX_ERROR]; + snprintf(err, sizeof(err), "Unsupported DRM pixel format: 0x%08X", f->format); + kvm_drm_copy_error_message(out_error, out_error_size, err); + return false; + } + + if (f->pitch < f->width * bpp) + { + kvm_drm_copy_error_message(out_error, out_error_size, "Invalid pitch for framebuffer"); + return false; + } + + size_t rgb_size = (size_t)f->width * (size_t)f->height * 3u; + if (rgb == NULL || rgb_capacity < rgb_size) + { + kvm_drm_copy_error_message(out_error, out_error_size, "Output RGB buffer too small"); + return false; + } + + uint32_t y; + if (expandTablesReady == 0) + { + for (i = 0; i < 32; ++i) { expand5[i] = (uint8_t)((i * 255u) / 31u); } + for (i = 0; i < 64; ++i) { expand6[i] = (uint8_t)((i * 255u) / 63u); } + expandTablesReady = 1; + } + + switch (f->format) + { + case DRM_FORMAT_XRGB8888: + case DRM_FORMAT_ARGB8888: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + d[0] = s[2]; + d[1] = s[1]; + d[2] = s[0]; + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_XBGR8888: + case DRM_FORMAT_ABGR8888: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_RGBX8888: + case DRM_FORMAT_RGBA8888: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + d[0] = s[3]; + d[1] = s[2]; + d[2] = s[1]; + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_BGRX8888: + case DRM_FORMAT_BGRA8888: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + d[0] = s[1]; + d[1] = s[2]; + d[2] = s[3]; + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_XRGB2101010: + case DRM_FORMAT_ARGB2101010: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + uint32_t v = ((uint32_t)s[0]) | (((uint32_t)s[1]) << 8) | (((uint32_t)s[2]) << 16) | (((uint32_t)s[3]) << 24); + d[0] = (uint8_t)(((v >> 20) & 0x3FFu) >> 2); + d[1] = (uint8_t)(((v >> 10) & 0x3FFu) >> 2); + d[2] = (uint8_t)(((v >> 0) & 0x3FFu) >> 2); + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_XBGR2101010: + case DRM_FORMAT_ABGR2101010: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + uint32_t v = ((uint32_t)s[0]) | (((uint32_t)s[1]) << 8) | (((uint32_t)s[2]) << 16) | (((uint32_t)s[3]) << 24); + d[0] = (uint8_t)(((v >> 0) & 0x3FFu) >> 2); + d[1] = (uint8_t)(((v >> 10) & 0x3FFu) >> 2); + d[2] = (uint8_t)(((v >> 20) & 0x3FFu) >> 2); + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_RGBX1010102: + case DRM_FORMAT_RGBA1010102: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + uint32_t v = ((uint32_t)s[0]) | (((uint32_t)s[1]) << 8) | (((uint32_t)s[2]) << 16) | (((uint32_t)s[3]) << 24); + d[0] = (uint8_t)(((v >> 22) & 0x3FFu) >> 2); + d[1] = (uint8_t)(((v >> 12) & 0x3FFu) >> 2); + d[2] = (uint8_t)(((v >> 2) & 0x3FFu) >> 2); + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_BGRX1010102: + case DRM_FORMAT_BGRA1010102: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + uint32_t v = ((uint32_t)s[0]) | (((uint32_t)s[1]) << 8) | (((uint32_t)s[2]) << 16) | (((uint32_t)s[3]) << 24); + d[0] = (uint8_t)(((v >> 2) & 0x3FFu) >> 2); + d[1] = (uint8_t)(((v >> 12) & 0x3FFu) >> 2); + d[2] = (uint8_t)(((v >> 22) & 0x3FFu) >> 2); + s += 4; + d += 3; + } + } + break; + } + case DRM_FORMAT_RGB888: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + d[0] = s[2]; + d[1] = s[1]; + d[2] = s[0]; + s += 3; + d += 3; + } + } + break; + } + case DRM_FORMAT_BGR888: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + s += 3; + d += 3; + } + } + break; + } + case DRM_FORMAT_RGB565: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + uint16_t v = ((uint16_t)s[0]) | (((uint16_t)s[1]) << 8); + d[0] = expand5[(v >> 11) & 0x1Fu]; + d[1] = expand6[(v >> 5) & 0x3Fu]; + d[2] = expand5[(v >> 0) & 0x1Fu]; + s += 2; + d += 3; + } + } + break; + } + case DRM_FORMAT_BGR565: + { + for (y = 0; y < f->height; ++y) + { + const uint8_t *s = src + ((size_t)y * (size_t)f->pitch); + uint8_t *d = rgb + ((size_t)y * (size_t)f->width * 3u); + uint32_t x = f->width; + while (x-- > 0) + { + uint16_t v = ((uint16_t)s[0]) | (((uint16_t)s[1]) << 8); + d[0] = expand5[(v >> 0) & 0x1Fu]; + d[1] = expand6[(v >> 5) & 0x3Fu]; + d[2] = expand5[(v >> 11) & 0x1Fu]; + s += 2; + d += 3; + } + } + break; + } + default: + { + char err[KVM_DRM_MAX_ERROR]; + snprintf(err, sizeof(err), "Unsupported DRM pixel format: 0x%08X", f->format); + kvm_drm_copy_error_message(out_error, out_error_size, err); + return false; + } + } + + if (rgb_size_out != NULL) + { + *rgb_size_out = rgb_size; + } + return true; +} + +static uint64_t kvm_drm_now_ms() +{ + struct timespec tsNow; + if (clock_gettime(CLOCK_MONOTONIC, &tsNow) != 0) + { + return 0; + } + return (((uint64_t)tsNow.tv_sec) * 1000ULL) + (((uint64_t)tsNow.tv_nsec) / 1000000ULL); +} + +static int kvm_drm_set_only_sys_admin_cap() +{ + struct __user_cap_header_struct header; + struct __user_cap_data_struct data[2]; + int cap = CAP_SYS_ADMIN; + + memset(&header, 0, sizeof(header)); + memset(data, 0, sizeof(data)); + + header.version = _LINUX_CAPABILITY_VERSION_3; + header.pid = 0; + + data[cap / 32].permitted = (1u << (cap % 32)); + data[cap / 32].effective = (1u << (cap % 32)); + data[cap / 32].inheritable = 0; + + return syscall(SYS_capset, &header, data); +} + +// Dropping caps with our DRM screen capture method requires more work than a simple setuid, because we need to +// retain CAP_SYS_ADMIN in order to scrape the screen. +static int kvm_drm_drop_to_session_uid_with_caps(int sessionUid, char *err, size_t errLen) +{ + struct passwd *pw = NULL; + uid_t targetUid = (uid_t)sessionUid; + uid_t currentUid = getuid(); + uid_t currentEuid = geteuid(); + + if (sessionUid == 0) { return 0; } + if (sessionUid < 0) + { + snprintf(err, errLen, "Invalid target uid: %d", sessionUid); + return -1; + } + if (targetUid == currentUid && currentEuid != 0) + { + // If we are already the requested user (eg, started directly from a shell), don't call + // initgroups()/setgid()/setuid() as those are root-only. Best-effort trim to CAP_SYS_ADMIN. + if (kvm_drm_set_only_sys_admin_cap() != 0) + { + snprintf(err, errLen, "capset(CAP_SYS_ADMIN) warning (errno=%d)", errno); + } + return 0; + } + if (currentEuid != 0) + { + snprintf(err, errLen, "Need root to switch to uid %d from uid %d", sessionUid, (int)currentUid); + return -1; + } + + pw = getpwuid(targetUid); + if (pw == NULL) + { + snprintf(err, errLen, "Unable to resolve passwd entry for uid %d (errno=%d)", sessionUid, errno); + return -1; + } + + if (prctl(PR_SET_KEEPCAPS, 1L, 0L, 0L, 0L) != 0) + { + snprintf(err, errLen, "PR_SET_KEEPCAPS failed (errno=%d)", errno); + return -1; + } + if (initgroups(pw->pw_name, pw->pw_gid) != 0) + { + snprintf(err, errLen, "initgroups(%s,%d) failed (errno=%d)", pw->pw_name, (int)pw->pw_gid, errno); + return -1; + } + if (setgid(pw->pw_gid) != 0) + { + snprintf(err, errLen, "setgid(%d) failed (errno=%d)", (int)pw->pw_gid, errno); + return -1; + } + if (setuid(targetUid) != 0) + { + snprintf(err, errLen, "setuid(%d) failed (errno=%d)", sessionUid, errno); + return -1; + } + if (kvm_drm_set_only_sys_admin_cap() != 0) + { + snprintf(err, errLen, "capset(CAP_SYS_ADMIN) failed (errno=%d)", errno); + return -1; + } + + ignore_result(prctl(PR_SET_KEEPCAPS, 0L, 0L, 0L, 0L)); + return 0; +} + +#endif + +void *kvm_server_mainloop_drm(void *parm) +{ + int sessionUid = (int)(intptr_t)parm; + char pchRequest2[30000]; + int ptr = 0; + int ptr2 = 0; + int len = 0; + ssize_t cbBytesRead = 0; + int r = 0; + struct sigaction action; + int displayListSent = 0; + uint64_t lastFrameTimeMs = 0; + int lastCaptureError = 0; + int64_t nFramesConverted = 0; + + g_kvmBackendDRM = 1; + g_enableEvents = kvm_events_evdev_init(); + if (!g_enableEvents) + { + kvm_send_error("evdev input injection unavailable"); + } + CURRENT_DISPLAY_ID = 0; + SCREEN_NUM = 0; + SCREEN_DEPTH = 24; + TILE_WIDTH = 32; + TILE_HEIGHT = 32; + COMPRESSION_RATIO = 50; + FRAME_RATE_TIMER = 33; + g_shutdown = 0; + + memset(&action, 0, sizeof(action)); + action.sa_sigaction = kvm_server_sighandler; + sigemptyset(&action.sa_mask); + action.sa_flags = SA_SIGINFO; + ignore_result(sigaction(SIGTERM, &action, NULL)); + +#if !defined(__linux__) + kvm_send_error("DRM capture backend is only supported on Linux"); + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + return (void *)-1; +#else + int fd = -1; + char err[KVM_DRM_MAX_ERROR]; + kvm_drm_output output; + kvm_drm_frame_map map; + kvm_drm_egl_context eglCtx; + unsigned char *rgbBuffer = NULL; + size_t rgbBufferSize = 0; + char *desktopBuffer = NULL; + long long desktopBufferSize = 0; + memset(&output, 0, sizeof(output)); + memset(&map, 0, sizeof(map)); + memset(&eglCtx, 0, sizeof(eglCtx)); + map.dma_fd = -1; + + char *explicitDevice = getenv("MESH_KVM_DRM_DEVICE"); + if (!kvm_drm_open_device_with_output(explicitDevice, &fd, &output, err, sizeof(err))) + { + kvm_send_error(err); + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + return (void *)-1; + } + if (kvm_drm_drop_master_if_held(fd) != 0) + { + snprintf(err, sizeof(err), "drmDropMaster failed (errno=%d)", errno); + kvm_send_error(err); + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + close(fd); + return (void *)-1; + } + + if (kvm_drm_drop_to_session_uid_with_caps(sessionUid, err, sizeof(err)) != 0) + { + fprintf(stderr, "DRM privilege setup failed: %s\n", err); + kvm_send_error(err); + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + close(fd); + return (void *)-1; + } + + while (!g_shutdown) + { + struct timeval tv; + fd_set readset; + fd_set errorset; + fd_set writeset; + int selectResult = 0; + + FD_ZERO(&readset); + FD_ZERO(&errorset); + FD_ZERO(&writeset); + tv.tv_sec = 0; + tv.tv_usec = 20000; + FD_SET(master2slave[0], &readset); + + selectResult = select(master2slave[0] + 1, &readset, &writeset, &errorset, &tv); + if (selectResult < 0) + { + if (errno == EINTR) + { + continue; + } + g_shutdown = 1; + break; + } + + if (selectResult > 0 && FD_ISSET(master2slave[0], &readset)) + { + cbBytesRead = read(master2slave[0], pchRequest2 + len, sizeof(pchRequest2) - len); + if (cbBytesRead <= 0) + { + g_shutdown = 1; + break; + } + + len += (int)cbBytesRead; + ptr = 0; + while ((ptr2 = kvm_server_inputdata((char *)pchRequest2 + ptr, len - ptr)) != 0) + { + ptr += ptr2; + } + if (ptr == len) + { + len = 0; + ptr = 0; + } + else if (ptr > 0) + { + memmove(pchRequest2, pchRequest2 + ptr, len - ptr); + len -= ptr; + ptr = 0; + } + } + + uint64_t nowMs = kvm_drm_now_ms(); + uint64_t frameInterval = FRAME_RATE_TIMER < 20 ? 20 : (uint64_t)FRAME_RATE_TIMER; + if (nowMs == 0 || (lastFrameTimeMs != 0 && nowMs - lastFrameTimeMs < frameInterval)) + { + continue; + } + lastFrameTimeMs = nowMs; + + if (g_pause || g_remotepause) + { + continue; + } + + kvm_drm_scanout_frame frame; + size_t rgbSize = 0; + bool converted = false; + memset(&frame, 0, sizeof(frame)); + + if (!kvm_drm_get_scanout_frame(fd, output.crtc_id, output.crtc_index, &frame, err, sizeof(err))) + { + if (lastCaptureError == 0) + { + kvm_send_error(err); + lastCaptureError = 1; + } + continue; + } + + if (frame.handle == 0 || frame.width == 0 || frame.height == 0) + { + if (lastCaptureError == 0) + { + kvm_send_error("DRM framebuffer is not readable (missing handle)"); + lastCaptureError = 1; + } + continue; + } + + if (SCREEN_WIDTH != (int)frame.width || SCREEN_HEIGHT != (int)frame.height) + { + int oldTileHeightCount = TILE_HEIGHT_COUNT; + SCREEN_WIDTH = (int)frame.width; + SCREEN_HEIGHT = (int)frame.height; + TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; + TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; + if (SCREEN_WIDTH % TILE_WIDTH) + { + TILE_WIDTH_COUNT++; + } + if (SCREEN_HEIGHT % TILE_HEIGHT) + { + TILE_HEIGHT_COUNT++; + } + kvm_send_resolution(); + kvm_send_display(); + reset_tile_info(oldTileHeightCount); + } + + if (!displayListSent) + { + kvm_send_display_list(); + displayListSent = 1; + } + + rgbSize = (size_t)frame.width * (size_t)frame.height * 3u; + if (rgbBufferSize < rgbSize) + { + unsigned char *tmp = (unsigned char *)realloc(rgbBuffer, rgbSize); + if (tmp == NULL) + ILIBCRITICALEXIT(254); + rgbBuffer = tmp; + rgbBufferSize = rgbSize; + } + + if (frame.modifier != DRM_FORMAT_MOD_INVALID && frame.modifier != DRM_FORMAT_MOD_LINEAR) + { + if (drm_debug && nFramesConverted == 0) + printf("Attempting GPU-assisted conversion for modifier 0x%016" PRIx64 "\n", frame.modifier); + + converted = kvm_drm_egl_convert_to_rgb24_gpu(&eglCtx, fd, frame.width, frame.height, frame.pitch, + frame.offset, frame.format, frame.handle, frame.modifier, + rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); + } + else + { + size_t required_bytes = (size_t)frame.offset + ((size_t)frame.pitch * (size_t)frame.height); + if (!kvm_drm_map_framebuffer_handle(fd, frame.handle, required_bytes, &map, err, sizeof(err))) + { + converted = false; + } + else + { + if (drm_debug && nFramesConverted == 0) + printf("Performing CPU readback conversion for format 0x%08X\n", frame.format); + + const uint8_t *src = map.addr + frame.offset; + converted = kvm_drm_convert_to_rgb24(&frame, src, rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); + } + } + + if (!converted) + { + if (lastCaptureError == 0) + { + kvm_send_error(err); + lastCaptureError = 1; + } + continue; + } + lastCaptureError = 0; + nFramesConverted++; + + if (g_tileInfo == NULL) + { + reset_tile_info(0); + } + + if (kvm_drm_send_dirty_tiles(rgbBuffer, rgbSize, &desktopBuffer, &desktopBufferSize) != 0) + { + g_shutdown = 1; + } + } + + if (desktopBuffer != NULL) + { + free(desktopBuffer); + desktopBuffer = NULL; + desktopBufferSize = 0; + } + if (jpeg_buffer != NULL) + { + free(jpeg_buffer); + jpeg_buffer = NULL; + jpeg_buffer_length = 0; + } + + if (rgbBuffer != NULL) + { + free(rgbBuffer); + rgbBuffer = NULL; + } + kvm_drm_destroy_map(&map); + kvm_drm_egl_destroy_context(&eglCtx); + if (fd >= 0) + { + close(fd); + fd = -1; + } + + close(slave2master[1]); + close(master2slave[0]); + slave2master[1] = 0; + master2slave[0] = 0; + + if (g_tileInfo != NULL) + { + for (r = 0; r < TILE_HEIGHT_COUNT; r++) + { + free(g_tileInfo[r]); + } + free(g_tileInfo); + g_tileInfo = NULL; + } + if (tilebuffer != NULL) + { + free(tilebuffer); + tilebuffer = NULL; + } + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + + return (void *)0; +#endif +} diff --git a/meshcore/KVM/Linux/linux_kvm_drm.h b/meshcore/KVM/Linux/linux_kvm_drm.h new file mode 100644 index 000000000..3a2f66770 --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_drm.h @@ -0,0 +1,28 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef LINUX_KVM_DRM_H_ +#define LINUX_KVM_DRM_H_ + +// The DRM screen capture system uses the Linux DRM (Direct Rendering Manager) subsystem to capture the screen. +// This is used when running under Wayland, as a catch-all mechanism that operates at a lower level than +// the screen compositor. + +extern int g_kvmBackendDRM; + +void* kvm_server_mainloop_drm(void* parm); + +#endif diff --git a/meshcore/KVM/Linux/linux_kvm_drm_egl.c b/meshcore/KVM/Linux/linux_kvm_drm_egl.c new file mode 100644 index 000000000..20286227b --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_drm_egl.c @@ -0,0 +1,549 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "linux_kvm_drm_egl.h" +#include "meshcore/meshdefines.h" + +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#include +#endif + +#if defined(__linux__) +#ifndef O_CLOEXEC +#define KVM_DRM_EGL_CLOEXEC 0 +#else +#define KVM_DRM_EGL_CLOEXEC O_CLOEXEC +#endif +#endif + +static void kvm_drm_egl_copy_error_message(char *dst, size_t dst_size, const char *src) +{ + if (dst == NULL || dst_size == 0) { return; } + if (src == NULL) + { + dst[0] = '\0'; + return; + } + + size_t n = strlen(src); + if (n >= dst_size) { n = dst_size - 1; } + memcpy(dst, src, n); + dst[n] = '\0'; +} + +#if defined(__linux__) + +static void kvm_drm_egl_format_egl_error(char *dst, size_t dst_size, const char *prefix, EGLint err) +{ + snprintf(dst, dst_size, "%s: 0x%04x", prefix, (unsigned int)err); +} + +static bool kvm_drm_egl_fail_with_persistent_error(kvm_drm_egl_context *g, char *out_error, size_t out_error_size, const char *msg) +{ + g->permanently_failed = true; + kvm_drm_egl_copy_error_message(g->fail_reason, sizeof(g->fail_reason), msg); + kvm_drm_egl_copy_error_message(out_error, out_error_size, msg); + return false; +} + +static GLuint kvm_drm_egl_compile_shader(GLenum type, const char *src, char *log_out, size_t log_size) +{ + GLuint shader = glCreateShader(type); + if (shader == 0) { return 0; } + + glShaderSource(shader, 1, &src, NULL); + glCompileShader(shader); + + GLint ok = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + if (ok != GL_TRUE) + { + if (log_out != NULL && log_size > 0) + { + GLint info_len = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_len); + if (info_len > 1) + { + GLint read_len = info_len; + if ((size_t)read_len >= log_size) { read_len = (GLint)(log_size - 1); } + glGetShaderInfoLog(shader, read_len, NULL, log_out); + log_out[read_len] = '\0'; + } + else + { + kvm_drm_egl_copy_error_message(log_out, log_size, "Shader compilation failed"); + } + } + + glDeleteShader(shader); + return 0; + } + + return shader; +} + +static bool kvm_drm_egl_init_gpu_readback(kvm_drm_egl_context *g, char *out_error, size_t out_error_size) +{ + if (g->initialized) { return true; } + if (g->permanently_failed) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, g->fail_reason); + return false; + } + + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXTFn = + (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (eglGetPlatformDisplayEXTFn != NULL) + { + g->dpy = eglGetPlatformDisplayEXTFn(EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, NULL); + } + if (g->dpy == EGL_NO_DISPLAY) + { + g->dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); + } + if (g->dpy == EGL_NO_DISPLAY) + { + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, "Failed to acquire EGL display"); + } + + EGLint major = 0; + EGLint minor = 0; + if (!eglInitialize(g->dpy, &major, &minor)) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglInitialize failed", eglGetError()); + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); + } + + if (!eglBindAPI(EGL_OPENGL_ES_API)) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglBindAPI(EGL_OPENGL_ES_API) failed", eglGetError()); + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); + } + + const EGLint cfg_attribs[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + EGLint num_cfg = 0; + if (!eglChooseConfig(g->dpy, cfg_attribs, &g->cfg, 1, &num_cfg) || num_cfg != 1) + { + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, "eglChooseConfig failed"); + } + + const EGLint ctx_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; + g->ctx = eglCreateContext(g->dpy, g->cfg, EGL_NO_CONTEXT, ctx_attribs); + if (g->ctx == EGL_NO_CONTEXT) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglCreateContext failed", eglGetError()); + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); + } + + const EGLint surf_attribs[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }; + g->surf = eglCreatePbufferSurface(g->dpy, g->cfg, surf_attribs); + if (g->surf == EGL_NO_SURFACE) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglCreatePbufferSurface failed", eglGetError()); + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); + } + g->surf_w = 1; + g->surf_h = 1; + + if (!eglMakeCurrent(g->dpy, g->surf, g->surf, g->ctx)) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglMakeCurrent failed", eglGetError()); + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); + } + + g->eglCreateImageKHRFn = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); + g->eglDestroyImageKHRFn = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); + g->glEGLImageTargetTexture2DOESFn = + (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); + if (g->eglCreateImageKHRFn == NULL || g->eglDestroyImageKHRFn == NULL || g->glEGLImageTargetTexture2DOESFn == NULL) + { + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, + "Missing required EGL/GLES extension entrypoints for dma-buf import"); + } + + const char *vs_src = "attribute vec2 aPos;\n" + "attribute vec2 aTex;\n" + "varying vec2 vTex;\n" + "void main(){ gl_Position=vec4(aPos,0.0,1.0); vTex=aTex; }\n"; + const char *fs_src = "precision mediump float;\n" + "varying vec2 vTex;\n" + "uniform sampler2D uTex;\n" + "void main(){ gl_FragColor = texture2D(uTex, vTex); }\n"; + + char vs_log[128] = { 0 }; + char fs_log[128] = { 0 }; + GLuint vs = kvm_drm_egl_compile_shader(GL_VERTEX_SHADER, vs_src, vs_log, sizeof(vs_log)); + GLuint fs = kvm_drm_egl_compile_shader(GL_FRAGMENT_SHADER, fs_src, fs_log, sizeof(fs_log)); + if (vs == 0 || fs == 0) + { + if (vs != 0) { glDeleteShader(vs); } + if (fs != 0) { glDeleteShader(fs); } + + char err[512] = { 0 }; + if (vs_log[0] != '\0' && fs_log[0] != '\0') + { + snprintf(err, sizeof(err), "Failed to compile GLES shaders [VS: %s] [FS: %s]", vs_log, fs_log); + } + else if (vs_log[0] != '\0') + { + snprintf(err, sizeof(err), "Failed to compile GLES shaders [VS: %s]", vs_log); + } + else if (fs_log[0] != '\0') + { + snprintf(err, sizeof(err), "Failed to compile GLES shaders [FS: %s]", fs_log); + } + else + { + kvm_drm_egl_copy_error_message(err, sizeof(err), "Failed to compile GLES shaders"); + } + + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); + } + + g->program = glCreateProgram(); + glAttachShader(g->program, vs); + glAttachShader(g->program, fs); + glLinkProgram(g->program); + glDeleteShader(vs); + glDeleteShader(fs); + + GLint linked = GL_FALSE; + glGetProgramiv(g->program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) + { + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, "Failed to link GLES program"); + } + + g->attr_pos = glGetAttribLocation(g->program, "aPos"); + g->attr_tex = glGetAttribLocation(g->program, "aTex"); + g->u_tex = glGetUniformLocation(g->program, "uTex"); + if (g->attr_pos < 0 || g->attr_tex < 0 || g->u_tex < 0) + { + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, + "Failed to resolve shader attribute/uniform locations"); + } + + const GLfloat quad[] = { + -1.0f, -1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 0.0f + }; + glGenBuffers(1, &g->vbo); + glBindBuffer(GL_ARRAY_BUFFER, g->vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW); + + glGenFramebuffers(1, &g->fbo); + glGenTextures(1, &g->out_tex); + + g->initialized = true; + return true; +} + +static bool kvm_drm_egl_ensure_gpu_target_size(kvm_drm_egl_context *g, int width, int height, char *out_error, size_t out_error_size) +{ + if (g->surf_w == width && g->surf_h == height) { return true; } + + if (g->surf != EGL_NO_SURFACE) + { + eglDestroySurface(g->dpy, g->surf); + g->surf = EGL_NO_SURFACE; + } + + const EGLint surf_attribs[] = { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE }; + g->surf = eglCreatePbufferSurface(g->dpy, g->cfg, surf_attribs); + if (g->surf == EGL_NO_SURFACE) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglCreatePbufferSurface(size) failed", eglGetError()); + kvm_drm_egl_copy_error_message(out_error, out_error_size, err); + return false; + } + g->surf_w = width; + g->surf_h = height; + + if (!eglMakeCurrent(g->dpy, g->surf, g->surf, g->ctx)) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglMakeCurrent(size) failed", eglGetError()); + kvm_drm_egl_copy_error_message(out_error, out_error_size, err); + return false; + } + + glBindTexture(GL_TEXTURE_2D, g->out_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + + glBindFramebuffer(GL_FRAMEBUFFER, g->fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g->out_tex, 0); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, "GPU readback framebuffer is incomplete"); + return false; + } + + return true; +} + +#endif + +bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint32_t width, uint32_t height, uint32_t pitch, + uint32_t offset, uint32_t format, uint32_t handle, uint64_t modifier, uint8_t *rgb, + size_t rgb_capacity, size_t *rgb_size_out, char *out_error, size_t out_error_size) +{ +#if !defined(__linux__) + UNREFERENCED_PARAMETER(ctx); + UNREFERENCED_PARAMETER(drm_fd); + UNREFERENCED_PARAMETER(width); + UNREFERENCED_PARAMETER(height); + UNREFERENCED_PARAMETER(pitch); + UNREFERENCED_PARAMETER(offset); + UNREFERENCED_PARAMETER(format); + UNREFERENCED_PARAMETER(handle); + UNREFERENCED_PARAMETER(modifier); + UNREFERENCED_PARAMETER(rgb); + UNREFERENCED_PARAMETER(rgb_capacity); + UNREFERENCED_PARAMETER(rgb_size_out); + kvm_drm_egl_copy_error_message(out_error, out_error_size, "DRM EGL GPU conversion is not supported on this platform"); + return false; +#else + if (ctx == NULL) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, "kvm_drm_egl context is null"); + return false; + } + + if (rgb_size_out != NULL) { *rgb_size_out = 0; } + + if (width == 0 || height == 0) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, "Invalid frame dimensions"); + return false; + } + if (width > (uint32_t)INT_MAX || height > (uint32_t)INT_MAX) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, "Frame dimensions are too large"); + return false; + } + + if (!kvm_drm_egl_init_gpu_readback(ctx, out_error, out_error_size)) { return false; } + if (!kvm_drm_egl_ensure_gpu_target_size(ctx, (int)width, (int)height, out_error, out_error_size)) { return false; } + + int dmabuf_fd = -1; + if (drmPrimeHandleToFD(drm_fd, handle, KVM_DRM_EGL_CLOEXEC | DRM_RDWR, &dmabuf_fd) != 0 || dmabuf_fd < 0) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, "drmPrimeHandleToFD failed for GPU path"); + return false; + } + + EGLint attrs[20]; + int a = 0; + attrs[a++] = EGL_WIDTH; + attrs[a++] = (EGLint)width; + attrs[a++] = EGL_HEIGHT; + attrs[a++] = (EGLint)height; + attrs[a++] = EGL_LINUX_DRM_FOURCC_EXT; + attrs[a++] = (EGLint)format; + attrs[a++] = EGL_DMA_BUF_PLANE0_FD_EXT; + attrs[a++] = dmabuf_fd; + attrs[a++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; + attrs[a++] = (EGLint)offset; + attrs[a++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; + attrs[a++] = (EGLint)pitch; + if (modifier != DRM_FORMAT_MOD_INVALID) + { + attrs[a++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; + attrs[a++] = (EGLint)(modifier & 0xFFFFFFFFu); + attrs[a++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; + attrs[a++] = (EGLint)((modifier >> 32) & 0xFFFFFFFFu); + } + attrs[a++] = EGL_NONE; + + EGLImageKHR image = ctx->eglCreateImageKHRFn(ctx->dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attrs); + close(dmabuf_fd); + if (image == EGL_NO_IMAGE_KHR) + { + char err[KVM_DRM_EGL_MAX_ERROR]; + kvm_drm_egl_format_egl_error(err, sizeof(err), "eglCreateImageKHR(dma-buf) failed", eglGetError()); + kvm_drm_egl_copy_error_message(out_error, out_error_size, err); + return false; + } + + GLuint src_tex = 0; + glGenTextures(1, &src_tex); + glBindTexture(GL_TEXTURE_2D, src_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + ctx->glEGLImageTargetTexture2DOESFn(GL_TEXTURE_2D, (GLeglImageOES)image); + + glBindFramebuffer(GL_FRAMEBUFFER, ctx->fbo); + glViewport(0, 0, (GLsizei)width, (GLsizei)height); + glUseProgram(ctx->program); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, src_tex); + glUniform1i(ctx->u_tex, 0); + glBindBuffer(GL_ARRAY_BUFFER, ctx->vbo); + glEnableVertexAttribArray((GLuint)ctx->attr_pos); + glEnableVertexAttribArray((GLuint)ctx->attr_tex); + glVertexAttribPointer((GLuint)ctx->attr_pos, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)(0)); + glVertexAttribPointer((GLuint)ctx->attr_tex, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (void*)(2 * sizeof(GLfloat))); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glDisableVertexAttribArray((GLuint)ctx->attr_pos); + glDisableVertexAttribArray((GLuint)ctx->attr_tex); + glFinish(); + + const size_t rgba_size = (size_t)width * (size_t)height * 4u; + if (ctx->rgba_readback_cap < rgba_size) + { + uint8_t *tmp = (uint8_t*)realloc(ctx->rgba_readback, rgba_size); + if (tmp == NULL) + { + ctx->eglDestroyImageKHRFn(ctx->dpy, image); + glDeleteTextures(1, &src_tex); + kvm_drm_egl_copy_error_message(out_error, out_error_size, "Failed to allocate temporary RGBA readback buffer"); + return false; + } + ctx->rgba_readback = tmp; + ctx->rgba_readback_cap = rgba_size; + } + + glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, GL_RGBA, GL_UNSIGNED_BYTE, ctx->rgba_readback); + + ctx->eglDestroyImageKHRFn(ctx->dpy, image); + glDeleteTextures(1, &src_tex); + + const size_t rgb_size = (size_t)width * (size_t)height * 3u; + if (rgb == NULL) + { + kvm_drm_egl_copy_error_message(out_error, out_error_size, "Output RGB pointer is null"); + return false; + } + if (rgb_capacity < rgb_size) + { + char msg[KVM_DRM_EGL_MAX_ERROR]; + snprintf(msg, sizeof(msg), "Output RGB buffer too small: need=%zu have=%zu", rgb_size, rgb_capacity); + kvm_drm_egl_copy_error_message(out_error, out_error_size, msg); + return false; + } + + for (uint32_t y = 0; y < height; ++y) + { + const uint8_t *src_row = ctx->rgba_readback + ((size_t)(height - 1u - y) * (size_t)width * 4u); + uint8_t *dst = rgb + ((size_t)y * (size_t)width * 3u); + for (uint32_t x = 0; x < width; ++x) + { + dst[0] = src_row[0]; + dst[1] = src_row[1]; + dst[2] = src_row[2]; + dst += 3; + src_row += 4; + } + } + + if (rgb_size_out != NULL) { *rgb_size_out = rgb_size; } + + return true; +#endif +} + +void kvm_drm_egl_destroy_context(kvm_drm_egl_context *ctx) +{ + if (ctx == NULL) { return; } + + free(ctx->rgba_readback); + ctx->rgba_readback = NULL; + ctx->rgba_readback_cap = 0; + +#if defined(__linux__) + if (ctx->program != 0) + { + glDeleteProgram(ctx->program); + ctx->program = 0; + } + if (ctx->vbo != 0) + { + glDeleteBuffers(1, &ctx->vbo); + ctx->vbo = 0; + } + if (ctx->fbo != 0) + { + glDeleteFramebuffers(1, &ctx->fbo); + ctx->fbo = 0; + } + if (ctx->out_tex != 0) + { + glDeleteTextures(1, &ctx->out_tex); + ctx->out_tex = 0; + } + + if (ctx->dpy != EGL_NO_DISPLAY) + { + if (ctx->ctx != EGL_NO_CONTEXT) + { + eglMakeCurrent(ctx->dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(ctx->dpy, ctx->ctx); + ctx->ctx = EGL_NO_CONTEXT; + } + if (ctx->surf != EGL_NO_SURFACE) + { + eglDestroySurface(ctx->dpy, ctx->surf); + ctx->surf = EGL_NO_SURFACE; + } + eglTerminate(ctx->dpy); + ctx->dpy = EGL_NO_DISPLAY; + } + + ctx->cfg = (EGLConfig)0; + ctx->surf_w = 0; + ctx->surf_h = 0; + ctx->eglCreateImageKHRFn = NULL; + ctx->eglDestroyImageKHRFn = NULL; + ctx->glEGLImageTargetTexture2DOESFn = NULL; + ctx->attr_pos = 0; + ctx->attr_tex = 0; + ctx->u_tex = 0; +#endif + + ctx->initialized = false; + ctx->permanently_failed = false; + ctx->fail_reason[0] = '\0'; +} diff --git a/meshcore/KVM/Linux/linux_kvm_drm_egl.h b/meshcore/KVM/Linux/linux_kvm_drm_egl.h new file mode 100644 index 000000000..c85475d89 --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_drm_egl.h @@ -0,0 +1,78 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef LINUX_KVM_DRM_EGL_H_ +#define LINUX_KVM_DRM_EGL_H_ + +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#include +#endif + +// What does this EGL code have to do with DRM screen capture? +// All we're doing here is blitting the entire screen from the GPU-native memory layout into +// a simple linear RGB buffer. At the point where we capture the frame via DRM, the memory +// can be in a GPU-native tiled memory layout. Instead of trying to cater for every possible +// GPU memory layout in present and future, we rather just use EGL to perform a full screen +// copy for us. +// This is not always required. Sometimes the DRM buffer is already in a linear format, +// in which case we skip this, and just perform pixel format conversion. + +#define KVM_DRM_EGL_MAX_ERROR 256 + +typedef struct kvm_drm_egl_context +{ + bool initialized; + bool permanently_failed; + char fail_reason[KVM_DRM_EGL_MAX_ERROR]; + +#if defined(__linux__) + EGLDisplay dpy; + EGLContext ctx; + EGLSurface surf; + EGLConfig cfg; + int surf_w; + int surf_h; + + PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHRFn; + PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHRFn; + PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOESFn; + + GLuint program; + GLuint vbo; + GLuint fbo; + GLuint out_tex; + GLint attr_pos; + GLint attr_tex; + GLint u_tex; +#endif + + uint8_t *rgba_readback; + size_t rgba_readback_cap; +} kvm_drm_egl_context; + +bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint32_t width, uint32_t height, uint32_t pitch, + uint32_t offset, uint32_t format, uint32_t handle, uint64_t modifier, uint8_t *rgb, + size_t rgb_capacity, size_t *rgb_size_out, char *out_error, size_t out_error_size); +void kvm_drm_egl_destroy_context(kvm_drm_egl_context *ctx); + +#endif diff --git a/meshcore/KVM/Linux/linux_kvm_wayland.c b/meshcore/KVM/Linux/linux_kvm_wayland.c new file mode 100644 index 000000000..3a903986d --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_wayland.c @@ -0,0 +1,95 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "linux_kvm_wayland.h" + +#include +#include +#include +#include +#include +#include + +// The "are we running under Wayland" detection is made more complicated by the fact that +// meshagent is usually running as a root daemon. If simply running in the context of a +// regular user in a desktop session, then you could just check one or two environment +// variables. + +static int kvm_wayland_socket_exists(const char *runtimeDir, const char *socketName) +{ + char socketPath[PATH_MAX]; + struct stat st; + int len = 0; + + if (runtimeDir == NULL || runtimeDir[0] == 0 || socketName == NULL || socketName[0] == 0) { return 0; } + len = snprintf(socketPath, sizeof(socketPath), "%s/%s", runtimeDir, socketName); + if (len <= 0 || len >= (int)sizeof(socketPath)) { return 0; } + if (stat(socketPath, &st) != 0) { return 0; } + return S_ISSOCK(st.st_mode) ? 1 : 0; +} + +int kvm_is_wayland_session_for_uid(int uid) +{ + char *sessionType = getenv("XDG_SESSION_TYPE"); + char *waylandDisplay = getenv("WAYLAND_DISPLAY"); + char *runtimeDir = getenv("XDG_RUNTIME_DIR"); + char fallbackRuntimeDir[64]; + int len = 0; + int fallbackUid = uid; + + if (fallbackUid < 0) + { + fallbackUid = (int)getuid(); + } + + if ((sessionType != NULL && strcasecmp(sessionType, "wayland") == 0) || + (waylandDisplay != NULL && waylandDisplay[0] != 0)) + { + return 1; + } + + if (runtimeDir != NULL) + { + if (waylandDisplay != NULL && kvm_wayland_socket_exists(runtimeDir, waylandDisplay)) { return 1; } + if (kvm_wayland_socket_exists(runtimeDir, "wayland-0") || kvm_wayland_socket_exists(runtimeDir, "wayland-1")) { return 1; } + } + + len = snprintf(fallbackRuntimeDir, sizeof(fallbackRuntimeDir), "/run/user/%d", fallbackUid); + if (len > 0 && len < (int)sizeof(fallbackRuntimeDir)) + { + if (waylandDisplay != NULL && kvm_wayland_socket_exists(fallbackRuntimeDir, waylandDisplay)) { return 1; } + if (kvm_wayland_socket_exists(fallbackRuntimeDir, "wayland-0") || kvm_wayland_socket_exists(fallbackRuntimeDir, "wayland-1")) { return 1; } + } + + return 0; +} + +kvm_screenreader_mode_t kvm_screenreader_mode() +{ + return kvm_screenreader_mode_for_uid(-1); +} + +kvm_screenreader_mode_t kvm_screenreader_mode_for_uid(int uid) +{ + if (kvm_is_wayland_session_for_uid(uid)) + { + printf("Using DRM/libevdev mode\n"); + return KVM_SCREENREADER_MODE_DRM; + } + + printf("Using X11 mode\n"); + return KVM_SCREENREADER_MODE_X11; +} diff --git a/meshcore/KVM/Linux/linux_kvm_wayland.h b/meshcore/KVM/Linux/linux_kvm_wayland.h new file mode 100644 index 000000000..6e1e6be17 --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_wayland.h @@ -0,0 +1,30 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef LINUX_KVM_WAYLAND_H_ +#define LINUX_KVM_WAYLAND_H_ + +typedef enum kvm_screenreader_mode_t +{ + KVM_SCREENREADER_MODE_X11 = 0, + KVM_SCREENREADER_MODE_DRM = 1 +} kvm_screenreader_mode_t; + +kvm_screenreader_mode_t kvm_screenreader_mode(); +kvm_screenreader_mode_t kvm_screenreader_mode_for_uid(int uid); +int kvm_is_wayland_session_for_uid(int uid); + +#endif diff --git a/meshcore/agentcore.c b/meshcore/agentcore.c index 4e215bfbc..41de26d0e 100644 --- a/meshcore/agentcore.c +++ b/meshcore/agentcore.c @@ -46,6 +46,7 @@ limitations under the License. #ifdef _POSIX #include #include +#include #endif #ifdef _OPENBSD @@ -58,13 +59,14 @@ int gRemoteMouseRenderDefault = 0; #ifdef WIN32 #include "KVM/Windows/kvm.h" #endif - #ifdef _POSIX - #ifndef __APPLE__ - #include "KVM/Linux/linux_kvm.h" - #else - #include "KVM/MacOS/mac_kvm.h" + #ifdef _POSIX + #ifndef __APPLE__ + #include "KVM/Linux/linux_kvm.h" + #include "KVM/Linux/linux_kvm_wayland.h" + #else + #include "KVM/MacOS/mac_kvm.h" + #endif #endif - #endif #endif #if defined(WIN32) && !defined(_WIN32_WCE) && !defined(_MINCORE) @@ -1350,12 +1352,12 @@ duk_ret_t ILibDuktape_MeshAgent_getRemoteDesktop(duk_context *ctx) // For Linux, we need to determine where the XAUTHORITY is: char *updateXAuth = NULL; char *updateDisplay = NULL; - char *xdm = NULL; + int waylandSession = kvm_is_wayland_session_for_uid(console_uid); int needPop = 0; duk_eval_string(ctx, "require('user-sessions').Self()"); int self = duk_get_int(ctx, -1); duk_pop(ctx); - if (self==0 || getenv("XAUTHORITY") == NULL || getenv("DISPLAY") == NULL) + if (!waylandSession && (self == 0 || getenv("XAUTHORITY") == NULL || getenv("DISPLAY") == NULL)) { if (duk_peval_string(ctx, "require('monitor-info').getXInfo") == 0) { @@ -1366,15 +1368,6 @@ duk_ret_t ILibDuktape_MeshAgent_getRemoteDesktop(duk_context *ctx) { updateXAuth = Duktape_GetStringPropertyValue(ctx, -1, "xauthority", NULL); updateDisplay = Duktape_GetStringPropertyValue(ctx, -1, "display", NULL); - xdm = Duktape_GetStringPropertyValue(ctx, -1, "xdm", ""); - - if (strcmp(xdm, "xwayland") == 0) - { - ILibDuktape_MeshAgent_RemoteDesktop_SendError(ptrs, "This platform is configured to use Xwayland"); - ILibDuktape_MeshAgent_RemoteDesktop_SendError(ptrs, "please modify config to use Xorg"); - duk_pop(ctx); - return(1); - } if (console_uid != 0 && updateXAuth == NULL) { From d8641606c0762acf5ae0d3064537b3e392ecac85 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Thu, 5 Mar 2026 15:49:50 +0200 Subject: [PATCH 02/14] Add new makefile linux package list for EGL/DRM --- makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/makefile b/makefile index 00aaa8f0f..9c4a588bd 100644 --- a/makefile +++ b/makefile @@ -7,12 +7,12 @@ # Then do "make -j8" and get the resulting file /.libs/libturbojpeg.a # # -# To build MeshAgent2 on Linux you first got to download the dev libraries to compile the agent, we need x11, txt, ext and jpeg. To install, do this: +# To build MeshAgent2 on Linux you first got to download the dev libraries to compile the agent, we need x11, txt, ext, egl, drm and jpeg. To install, do this: # Using APT: -# sudo apt-get install libx11-dev libxtst-dev libxext-dev libjpeg62-dev +# sudo apt-get install libx11-dev libxtst-dev libxext-dev libjpeg62-dev libegl1-mesa-dev libdrm-dev pkg-config # # Using YUM: -# sudo yum install libX11-devel libXtst-devel libXext-devel libjpeg-devel +# sudo yum install libX11-devel libXtst-devel libXext-devel libjpeg-devel mesa-libEGL-devel libdrm-devel pkgconf # # NOTE: If you install headers for jpeg8, you need to put the compiled .a in the v80 folder, and specify JPEGVER=v80 when building MeshAgent # eg: make linux ARCHID=6 JPEGVER=v80 From 1045c60390820370d3f8ec44e7a059c4e8df99c3 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Fri, 6 Mar 2026 08:28:17 +0200 Subject: [PATCH 03/14] Fix signature of ValidateMeshServer This fixes a warning on modern compilers --- meshcore/agentcore.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meshcore/agentcore.c b/meshcore/agentcore.c index 41de26d0e..f330e8ee6 100644 --- a/meshcore/agentcore.c +++ b/meshcore/agentcore.c @@ -4344,8 +4344,10 @@ void MeshServer_Connect(MeshAgentHostContainer *agent) } #ifndef MICROSTACK_NOTLS -int ValidateMeshServer(ILibWebClient_RequestToken sender, int preverify_ok, STACK_OF(X509) *certs, struct sockaddr_in6 *address, MeshAgentHostContainer *agent) +int ValidateMeshServer(ILibWebClient_RequestToken sender, int preverify_ok, STACK_OF(X509) *certs, struct sockaddr_in6 *address, void *user) { + MeshAgentHostContainer *agent = (MeshAgentHostContainer*)user; + int len = ILibSimpleDataStore_Get(agent->masterDb, "validateWebCert", ILibScratchPad, sizeof(ILibScratchPad)); // Values here are 0 terminated, but the 0 is counted in size, so add one to the length check. if ((len == 2 && strncmp("1", ILibScratchPad, 1) == 0) || From b01a857b05863048eb9cd41902b8232c0a82bebf Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Fri, 6 Mar 2026 08:31:04 +0200 Subject: [PATCH 04/14] Add a Wayland detection mechanism when there's no Xorg/X11 on the system Previously, we only worked on eg Ubuntu 24.04, where they still shipped Xorg/X11. Without this change the agent does not advertise desktop capability, and the "Desktop" tab in MeshCentral is unavailable. --- microscript/ILibDuktape_Polyfills.c | 15 +++++++++++- modules/monitor-info.js | 38 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/microscript/ILibDuktape_Polyfills.c b/microscript/ILibDuktape_Polyfills.c index 4d84b7b28..81aa51860 100644 --- a/microscript/ILibDuktape_Polyfills.c +++ b/microscript/ILibDuktape_Polyfills.c @@ -2610,7 +2610,20 @@ void ILibDuktape_Polyfills_JS_Init(duk_context *ctx) duk_peval_string_noresult(ctx, "addCompressedModule('user-sessions', Buffer.from('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', 'base64'), '2022-03-29T11:33:55.000-07:00');"); - // Mesh Agent NodeID helper, refer to modules/_agentNodeId.js + // Linux Wayland fallback: if monitor-info reports false because no X server is found, + // but an active Wayland socket exists, raise the same KVM support signal. + duk_peval_string_noresult(ctx, + "try{if(process.platform==='linux'){var fs=require('fs');var us=require('user-sessions');" + "var mi=require('monitor-info');if(mi.kvm_x11_support!==true){" + "var uids=us.consoleUid({active:true});if(!Array.isArray(uids)){uids=[uids];}" + "if(uids.length===0){uids=[us.consoleUid()];}var wd=process.env['WAYLAND_DISPLAY'];" + "for(var i=0;i0)){continue;}" + "var rd='/run/user/'+uid;" + "if((wd&&fs.existsSync(rd+'/'+wd))||fs.existsSync(rd+'/wayland-0')||fs.existsSync(rd+'/wayland-1')){" + "mi.kvm_x11_serverFound=true;if(typeof mi.emit==='function'){mi.emit('kvmSupportDetected',true);}break;}}}}}" + "catch(__mesh_wayland_ex){}"); + + // Mesh Agent NodeID helper, refer to modules/_agentNodeId.js duk_peval_string_noresult(ctx, "addCompressedModule('_agentNodeId', Buffer.from('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', 'base64'), '2022-06-03T01:08:06.000-07:00');"); // Mesh Agent Status Helper, refer to modules/_agentStatus.js diff --git a/modules/monitor-info.js b/modules/monitor-info.js index ec4f65a7b..2194772c0 100644 --- a/modules/monitor-info.js +++ b/modules/monitor-info.js @@ -317,9 +317,45 @@ function monitorinfo() MWM_FUNC_CLOSE : (1 << 5) }; this._xtries = 0; + this._kvmcheck_wayland = function _kvmcheck_wayland() + { + if (process.platform != 'linux') { return false; } + try + { + var fs = require('fs'); + var uid = 0; + + try + { + uid = require('user-sessions').consoleUid({ active: true }); + if (Array.isArray(uid)) { uid = (uid.length > 0 ? parseInt(uid[0]) : 0); } + } + catch (z) { } + if (uid == null || uid <= 0) { try { uid = require('user-sessions').consoleUid(); } catch (zz) { uid = 0; } } + + if (uid != null && uid > 0) + { + var runtimeDir = '/run/user/' + uid; + var waylandDisplay = process.env['WAYLAND_DISPLAY']; + + if (waylandDisplay && fs.existsSync(runtimeDir + '/' + waylandDisplay)) { return true; } + if (fs.existsSync(runtimeDir + '/wayland-0') || fs.existsSync(runtimeDir + '/wayland-1')) { return true; } + } + } + catch (e) + { + } + return false; + }; this._kvmcheck = function _kvmcheck() { var retry = false; + if (this._kvmcheck_wayland()) + { + this.kvm_x11_serverFound = true; + this.emit('kvmSupportDetected', true); + return; + } if (!(this.Location_X11LIB && this.Location_X11TST && this.Location_X11EXT)) { this._check(); @@ -397,7 +433,7 @@ function monitorinfo() if (ch.stdout.str.trim() != '') { // X Server found - Object.defineProperty(this, 'kvm_x11_serverFound', { value: true }); + this.kvm_x11_serverFound = true; this.emit('kvmSupportDetected', true); } else From 56d61a92b30f8eba4c0985301b66512466d4ef54 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Fri, 6 Mar 2026 11:13:50 +0200 Subject: [PATCH 05/14] Make wayland sticky This prevents us from switching back to X11 mode if we connect at the greeter screen. We may still be able to simplify the wayland detection to just the seat0 read. --- meshcore/KVM/Linux/linux_kvm_wayland.c | 77 ++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/meshcore/KVM/Linux/linux_kvm_wayland.c b/meshcore/KVM/Linux/linux_kvm_wayland.c index 3a903986d..ae5ff9911 100644 --- a/meshcore/KVM/Linux/linux_kvm_wayland.c +++ b/meshcore/KVM/Linux/linux_kvm_wayland.c @@ -19,6 +19,7 @@ limitations under the License. #include #include #include +#include #include #include #include @@ -28,6 +29,62 @@ limitations under the License. // regular user in a desktop session, then you could just check one or two environment // variables. +// Once the agent has positively identified a Wayland-backed console, keep using the +// DRM/libevdev path for the remainder of the process lifetime. +// +// Without this stickyness, the following sequence ends up erroneously switching us back to X11: +// 1. Login as userX +// 2. Connect mesh remote desktop +// 3. Logout from userX (switching back to greeter) +// 4. Disconnect mesh, and reconnect mesh remote desktop. +// 5. Agent is now in X11 mode... +// The above sequence was reproducible on Ubuntu 25.10 +static int g_kvmWaylandSticky = 0; + +static int kvm_read_systemd_value(const char *path, const char *key, char *value, size_t valueLen) +{ + FILE *f = NULL; + char line[512]; + size_t keyLen = 0; + + if (path == NULL || key == NULL || value == NULL || valueLen == 0) { return 0; } + f = fopen(path, "r"); + if (f == NULL) { return 0; } + keyLen = strlen(key); + while (fgets(line, sizeof(line), f) != NULL) + { + if (strncmp(line, key, keyLen) == 0) + { + char *tmp = line + keyLen; + tmp[strcspn(tmp, "\r\n")] = 0; + snprintf(value, valueLen, "%s", tmp); + fclose(f); + return 1; + } + } + fclose(f); + return 0; +} + +static int kvm_active_seat0_session_is_wayland(void) +{ + char activeSession[64]; + char type[64]; + char seatPath[PATH_MAX]; + int len = 0; + + memset(activeSession, 0, sizeof(activeSession)); + memset(type, 0, sizeof(type)); + if (!kvm_read_systemd_value("/run/systemd/seats/seat0", "ACTIVE=", activeSession, sizeof(activeSession))) { return 0; } + if (activeSession[0] == 0) { return 0; } + + len = snprintf(seatPath, sizeof(seatPath), "/run/systemd/sessions/%s", activeSession); + if (len <= 0 || len >= (int)sizeof(seatPath)) { return 0; } + if (!kvm_read_systemd_value(seatPath, "TYPE=", type, sizeof(type))) { return 0; } + + return strcasecmp(type, "wayland") == 0 ? 1 : 0; +} + static int kvm_wayland_socket_exists(const char *runtimeDir, const char *socketName) { char socketPath[PATH_MAX]; @@ -55,23 +112,35 @@ int kvm_is_wayland_session_for_uid(int uid) fallbackUid = (int)getuid(); } + if (g_kvmWaylandSticky != 0) + { + return 1; + } + if ((sessionType != NULL && strcasecmp(sessionType, "wayland") == 0) || (waylandDisplay != NULL && waylandDisplay[0] != 0)) { + g_kvmWaylandSticky = 1; + return 1; + } + + if (kvm_active_seat0_session_is_wayland()) + { + g_kvmWaylandSticky = 1; return 1; } if (runtimeDir != NULL) { - if (waylandDisplay != NULL && kvm_wayland_socket_exists(runtimeDir, waylandDisplay)) { return 1; } - if (kvm_wayland_socket_exists(runtimeDir, "wayland-0") || kvm_wayland_socket_exists(runtimeDir, "wayland-1")) { return 1; } + if (waylandDisplay != NULL && kvm_wayland_socket_exists(runtimeDir, waylandDisplay)) { g_kvmWaylandSticky = 1; return 1; } + if (kvm_wayland_socket_exists(runtimeDir, "wayland-0") || kvm_wayland_socket_exists(runtimeDir, "wayland-1")) { g_kvmWaylandSticky = 1; return 1; } } len = snprintf(fallbackRuntimeDir, sizeof(fallbackRuntimeDir), "/run/user/%d", fallbackUid); if (len > 0 && len < (int)sizeof(fallbackRuntimeDir)) { - if (waylandDisplay != NULL && kvm_wayland_socket_exists(fallbackRuntimeDir, waylandDisplay)) { return 1; } - if (kvm_wayland_socket_exists(fallbackRuntimeDir, "wayland-0") || kvm_wayland_socket_exists(fallbackRuntimeDir, "wayland-1")) { return 1; } + if (waylandDisplay != NULL && kvm_wayland_socket_exists(fallbackRuntimeDir, waylandDisplay)) { g_kvmWaylandSticky = 1; return 1; } + if (kvm_wayland_socket_exists(fallbackRuntimeDir, "wayland-0") || kvm_wayland_socket_exists(fallbackRuntimeDir, "wayland-1")) { g_kvmWaylandSticky = 1; return 1; } } return 0; From 8fb86804277690bdc57bfe5efae40eb1e0b444e3 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Fri, 6 Mar 2026 13:13:52 +0200 Subject: [PATCH 06/14] Delete hacked-in polyfills.c code It was already in monitor-info.js --- microscript/ILibDuktape_Polyfills.c | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/microscript/ILibDuktape_Polyfills.c b/microscript/ILibDuktape_Polyfills.c index 81aa51860..fbabd7adc 100644 --- a/microscript/ILibDuktape_Polyfills.c +++ b/microscript/ILibDuktape_Polyfills.c @@ -2580,7 +2580,7 @@ void ILibDuktape_Polyfills_JS_Init(duk_context *ctx) #endif // monitor-info: Refer to modules/monitor-info.js - duk_peval_string_noresult(ctx, "addCompressedModule('monitor-info', Buffer.from('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', 'base64'), '2022-04-02T20:22:49.000-07:00');"); + duk_peval_string_noresult(ctx, "addCompressedModule('monitor-info', Buffer.from('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', 'base64'), '2026-03-05T18:48:14.000+02:00');"); // service-host. Refer to modules/service-host.js duk_peval_string_noresult(ctx, "addCompressedModule('service-host', Buffer.from('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', 'base64'));"); @@ -2610,19 +2610,6 @@ void ILibDuktape_Polyfills_JS_Init(duk_context *ctx) duk_peval_string_noresult(ctx, "addCompressedModule('user-sessions', Buffer.from('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', 'base64'), '2022-03-29T11:33:55.000-07:00');"); - // Linux Wayland fallback: if monitor-info reports false because no X server is found, - // but an active Wayland socket exists, raise the same KVM support signal. - duk_peval_string_noresult(ctx, - "try{if(process.platform==='linux'){var fs=require('fs');var us=require('user-sessions');" - "var mi=require('monitor-info');if(mi.kvm_x11_support!==true){" - "var uids=us.consoleUid({active:true});if(!Array.isArray(uids)){uids=[uids];}" - "if(uids.length===0){uids=[us.consoleUid()];}var wd=process.env['WAYLAND_DISPLAY'];" - "for(var i=0;i0)){continue;}" - "var rd='/run/user/'+uid;" - "if((wd&&fs.existsSync(rd+'/'+wd))||fs.existsSync(rd+'/wayland-0')||fs.existsSync(rd+'/wayland-1')){" - "mi.kvm_x11_serverFound=true;if(typeof mi.emit==='function'){mi.emit('kvmSupportDetected',true);}break;}}}}}" - "catch(__mesh_wayland_ex){}"); - // Mesh Agent NodeID helper, refer to modules/_agentNodeId.js duk_peval_string_noresult(ctx, "addCompressedModule('_agentNodeId', Buffer.from('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', 'base64'), '2022-06-03T01:08:06.000-07:00');"); From be4de8004026086bf391f2b77a40045296fa3f2b Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Mon, 9 Mar 2026 07:31:28 +0200 Subject: [PATCH 07/14] Add more logging and error checking to DRM/EGL code paths Aiming to find the reason why we're failing on i915 --- meshcore/KVM/Linux/linux_kvm_drm.c | 135 ++++++++++++++++++++++++- meshcore/KVM/Linux/linux_kvm_drm_egl.c | 98 +++++++++++++++++- 2 files changed, 227 insertions(+), 6 deletions(-) diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index c8884c92c..7cdce6254 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -268,6 +268,48 @@ static void kvm_drm_copy_error_message(char *dst, size_t dst_size, const char *s dst[n] = 0; } +static void kvm_drm_format_fourcc(char *dst, size_t dst_size, uint32_t format) +{ + char code[5]; + int i; + + if (dst == NULL || dst_size == 0) + { + return; + } + + for (i = 0; i < 4; ++i) + { + unsigned char ch = (unsigned char)((format >> (i * 8)) & 0xFFu); + code[i] = (char)((ch >= 32 && ch <= 126) ? ch : '.'); + } + code[4] = 0; + snprintf(dst, dst_size, "%s/0x%08X", code, format); +} + +static void kvm_drm_debug_log_scanout_frame(const char *prefix, const kvm_drm_scanout_frame *frame) +{ + char format[32]; + + if (!drm_debug || frame == NULL) + { + return; + } + + kvm_drm_format_fourcc(format, sizeof(format), frame->format); + fprintf(stderr, + "DRM: %s fb_id=%u size=%ux%u pitch=%u offset=%u format=%s modifier=0x%016" PRIx64 " handle=%u\n", + (prefix != NULL) ? prefix : "scanout", + frame->fb_id, + frame->width, + frame->height, + frame->pitch, + frame->offset, + format, + frame->modifier, + frame->handle); +} + static const char *kvm_drm_connector_type_name(uint32_t t) { switch (t) @@ -473,6 +515,11 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o out->connector_id = conn->connector_id; out->crtc_id = crtc_id; out->crtc_index = crtc_index; + if (drm_debug) + { + fprintf(stderr, "DRM: Selected output %s on %s (connector=%u crtc=%u index=%d)\n", + out->connector_name, out->device_path, out->connector_id, out->crtc_id, out->crtc_index); + } found = true; drmModeFreeConnector(conn); @@ -604,6 +651,10 @@ static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, if (fb_id == 0) { fb_id = kvm_drm_get_plane_fb_id(fd, crtc_id, crtc_index); + if (drm_debug && fb_id != 0) + { + fprintf(stderr, "DRM: CRTC %u has no direct buffer_id, using plane framebuffer %u\n", crtc_id, fb_id); + } } if (fb_id == 0) { @@ -615,6 +666,44 @@ static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, drmModeFB2 *fb2 = drmModeGetFB2(fd, fb_id); if (fb2 != NULL) { + int planeCount = 1; + while (planeCount < 4 && + (fb2->handles[planeCount] != 0 || fb2->pitches[planeCount] != 0 || fb2->offsets[planeCount] != 0)) + { + ++planeCount; + } + + if (drm_debug) + { + char format[32]; + kvm_drm_format_fourcc(format, sizeof(format), fb2->pixel_format); + fprintf(stderr, + "DRM: FB2 fb_id=%u planes=%d size=%ux%u format=%s modifier=0x%016" PRIx64 + " handles=[%u,%u,%u,%u] pitches=[%u,%u,%u,%u] offsets=[%u,%u,%u,%u]\n", + fb_id, + planeCount, + fb2->width, + fb2->height, + format, + fb2->modifier, + fb2->handles[0], fb2->handles[1], fb2->handles[2], fb2->handles[3], + fb2->pitches[0], fb2->pitches[1], fb2->pitches[2], fb2->pitches[3], + fb2->offsets[0], fb2->offsets[1], fb2->offsets[2], fb2->offsets[3]); + } + + if (planeCount > 1) + { + char err[KVM_DRM_MAX_ERROR]; + char format[32]; + kvm_drm_format_fourcc(format, sizeof(format), fb2->pixel_format); + snprintf(err, sizeof(err), + "Framebuffer %u uses %d DRM planes (%s, modifier=0x%016" PRIx64 "); multi-plane scanout is not supported by this capture path", + fb_id, planeCount, format, fb2->modifier); + drmModeFreeFB2(fb2); + kvm_drm_copy_error_message(out_error, out_error_size, err); + return false; + } + if (fb2->handles[1] == 0 && fb2->handles[2] == 0 && fb2->handles[3] == 0) { out->width = fb2->width; @@ -1135,6 +1224,15 @@ void *kvm_server_mainloop_drm(void *parm) size_t rgbBufferSize = 0; char *desktopBuffer = NULL; long long desktopBufferSize = 0; + uint32_t lastLoggedFbId = 0; + uint32_t lastLoggedWidth = 0; + uint32_t lastLoggedHeight = 0; + uint32_t lastLoggedPitch = 0; + uint32_t lastLoggedOffset = 0; + uint32_t lastLoggedFormat = 0; + uint32_t lastLoggedHandle = 0; + uint64_t lastLoggedModifier = UINT64_MAX; + int lastLoggedPath = -1; memset(&output, 0, sizeof(output)); memset(&map, 0, sizeof(map)); memset(&eglCtx, 0, sizeof(eglCtx)); @@ -1263,6 +1361,27 @@ void *kvm_server_mainloop_drm(void *parm) continue; } + if (drm_debug && + (frame.fb_id != lastLoggedFbId || + frame.width != lastLoggedWidth || + frame.height != lastLoggedHeight || + frame.pitch != lastLoggedPitch || + frame.offset != lastLoggedOffset || + frame.format != lastLoggedFormat || + frame.handle != lastLoggedHandle || + frame.modifier != lastLoggedModifier)) + { + kvm_drm_debug_log_scanout_frame("Using scanout framebuffer", &frame); + lastLoggedFbId = frame.fb_id; + lastLoggedWidth = frame.width; + lastLoggedHeight = frame.height; + lastLoggedPitch = frame.pitch; + lastLoggedOffset = frame.offset; + lastLoggedFormat = frame.format; + lastLoggedHandle = frame.handle; + lastLoggedModifier = frame.modifier; + } + if (SCREEN_WIDTH != (int)frame.width || SCREEN_HEIGHT != (int)frame.height) { int oldTileHeightCount = TILE_HEIGHT_COUNT; @@ -1301,8 +1420,11 @@ void *kvm_server_mainloop_drm(void *parm) if (frame.modifier != DRM_FORMAT_MOD_INVALID && frame.modifier != DRM_FORMAT_MOD_LINEAR) { - if (drm_debug && nFramesConverted == 0) - printf("Attempting GPU-assisted conversion for modifier 0x%016" PRIx64 "\n", frame.modifier); + if (drm_debug && lastLoggedPath != 1) + { + fprintf(stderr, "DRM: Using GPU-assisted conversion for modifier 0x%016" PRIx64 "\n", frame.modifier); + lastLoggedPath = 1; + } converted = kvm_drm_egl_convert_to_rgb24_gpu(&eglCtx, fd, frame.width, frame.height, frame.pitch, frame.offset, frame.format, frame.handle, frame.modifier, @@ -1317,8 +1439,13 @@ void *kvm_server_mainloop_drm(void *parm) } else { - if (drm_debug && nFramesConverted == 0) - printf("Performing CPU readback conversion for format 0x%08X\n", frame.format); + if (drm_debug && lastLoggedPath != 0) + { + char format[32]; + kvm_drm_format_fourcc(format, sizeof(format), frame.format); + fprintf(stderr, "DRM: Using CPU readback conversion for format %s\n", format); + lastLoggedPath = 0; + } const uint8_t *src = map.addr + frame.offset; converted = kvm_drm_convert_to_rgb24(&frame, src, rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); diff --git a/meshcore/KVM/Linux/linux_kvm_drm_egl.c b/meshcore/KVM/Linux/linux_kvm_drm_egl.c index 20286227b..b63122e0d 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm_egl.c +++ b/meshcore/KVM/Linux/linux_kvm_drm_egl.c @@ -17,12 +17,14 @@ limitations under the License. #include "linux_kvm_drm_egl.h" #include "meshcore/meshdefines.h" +#include #include #include #include #include #if defined(__linux__) +#include #include #include #include @@ -54,11 +56,65 @@ static void kvm_drm_egl_copy_error_message(char *dst, size_t dst_size, const cha #if defined(__linux__) +static void kvm_drm_egl_format_fourcc(char *dst, size_t dst_size, uint32_t format) +{ + char code[5]; + int i; + + if (dst == NULL || dst_size == 0) + { + return; + } + + for (i = 0; i < 4; ++i) + { + unsigned char ch = (unsigned char)((format >> (i * 8)) & 0xFFu); + code[i] = (char)((ch >= 32 && ch <= 126) ? ch : '.'); + } + code[4] = '\0'; + snprintf(dst, dst_size, "%s/0x%08X", code, format); +} + static void kvm_drm_egl_format_egl_error(char *dst, size_t dst_size, const char *prefix, EGLint err) { snprintf(dst, dst_size, "%s: 0x%04x", prefix, (unsigned int)err); } +static const char *kvm_drm_egl_gl_error_name(GLenum err) +{ + switch (err) + { + case GL_INVALID_ENUM: + return "GL_INVALID_ENUM"; + case GL_INVALID_VALUE: + return "GL_INVALID_VALUE"; + case GL_INVALID_OPERATION: + return "GL_INVALID_OPERATION"; + case GL_INVALID_FRAMEBUFFER_OPERATION: + return "GL_INVALID_FRAMEBUFFER_OPERATION"; + case GL_OUT_OF_MEMORY: + return "GL_OUT_OF_MEMORY"; + default: + return "GL_UNKNOWN_ERROR"; + } +} + +static bool kvm_drm_egl_check_gl_error(const char *stage, char *out_error, size_t out_error_size) +{ + GLenum err = glGetError(); + if (err == GL_NO_ERROR) + { + return true; + } + + snprintf(out_error, out_error_size, "%s failed: %s (0x%04x)", + stage, + kvm_drm_egl_gl_error_name(err), + (unsigned int)err); + while (glGetError() != GL_NO_ERROR) {} + return false; +} + static bool kvm_drm_egl_fail_with_persistent_error(kvm_drm_egl_context *g, char *out_error, size_t out_error_size, const char *msg) { g->permanently_failed = true; @@ -135,6 +191,11 @@ static bool kvm_drm_egl_init_gpu_readback(kvm_drm_egl_context *g, char *out_erro kvm_drm_egl_format_egl_error(err, sizeof(err), "eglInitialize failed", eglGetError()); return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); } + fprintf(stderr, "DRM EGL: Initialized EGL %d.%d vendor=%s version=%s\n", + (int)major, + (int)minor, + eglQueryString(g->dpy, EGL_VENDOR) != NULL ? eglQueryString(g->dpy, EGL_VENDOR) : "unknown", + eglQueryString(g->dpy, EGL_VERSION) != NULL ? eglQueryString(g->dpy, EGL_VERSION) : "unknown"); if (!eglBindAPI(EGL_OPENGL_ES_API)) { @@ -369,7 +430,9 @@ bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint int dmabuf_fd = -1; if (drmPrimeHandleToFD(drm_fd, handle, KVM_DRM_EGL_CLOEXEC | DRM_RDWR, &dmabuf_fd) != 0 || dmabuf_fd < 0) { - kvm_drm_egl_copy_error_message(out_error, out_error_size, "drmPrimeHandleToFD failed for GPU path"); + char msg[KVM_DRM_EGL_MAX_ERROR]; + snprintf(msg, sizeof(msg), "drmPrimeHandleToFD failed for GPU path (handle=%u errno=%d)", handle, errno); + kvm_drm_egl_copy_error_message(out_error, out_error_size, msg); return false; } @@ -401,7 +464,13 @@ bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint if (image == EGL_NO_IMAGE_KHR) { char err[KVM_DRM_EGL_MAX_ERROR]; - kvm_drm_egl_format_egl_error(err, sizeof(err), "eglCreateImageKHR(dma-buf) failed", eglGetError()); + char formatName[32]; + kvm_drm_egl_format_fourcc(formatName, sizeof(formatName), format); + snprintf(err, sizeof(err), + "eglCreateImageKHR(dma-buf) failed for %s modifier=0x%016" PRIx64 ": 0x%04x", + formatName, + modifier, + (unsigned int)eglGetError()); kvm_drm_egl_copy_error_message(out_error, out_error_size, err); return false; } @@ -414,8 +483,21 @@ bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ctx->glEGLImageTargetTexture2DOESFn(GL_TEXTURE_2D, (GLeglImageOES)image); + if (!kvm_drm_egl_check_gl_error("glEGLImageTargetTexture2DOES", out_error, out_error_size)) + { + ctx->eglDestroyImageKHRFn(ctx->dpy, image); + glDeleteTextures(1, &src_tex); + return false; + } glBindFramebuffer(GL_FRAMEBUFFER, ctx->fbo); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + ctx->eglDestroyImageKHRFn(ctx->dpy, image); + glDeleteTextures(1, &src_tex); + kvm_drm_egl_copy_error_message(out_error, out_error_size, "GPU readback framebuffer became incomplete"); + return false; + } glViewport(0, 0, (GLsizei)width, (GLsizei)height); glUseProgram(ctx->program); glActiveTexture(GL_TEXTURE0); @@ -430,6 +512,12 @@ bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint glDisableVertexAttribArray((GLuint)ctx->attr_pos); glDisableVertexAttribArray((GLuint)ctx->attr_tex); glFinish(); + if (!kvm_drm_egl_check_gl_error("GPU draw/readback pass", out_error, out_error_size)) + { + ctx->eglDestroyImageKHRFn(ctx->dpy, image); + glDeleteTextures(1, &src_tex); + return false; + } const size_t rgba_size = (size_t)width * (size_t)height * 4u; if (ctx->rgba_readback_cap < rgba_size) @@ -447,6 +535,12 @@ bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint } glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, GL_RGBA, GL_UNSIGNED_BYTE, ctx->rgba_readback); + if (!kvm_drm_egl_check_gl_error("glReadPixels", out_error, out_error_size)) + { + ctx->eglDestroyImageKHRFn(ctx->dpy, image); + glDeleteTextures(1, &src_tex); + return false; + } ctx->eglDestroyImageKHRFn(ctx->dpy, image); glDeleteTextures(1, &src_tex); From 501fc95011da2eea50c3c5cac18b69808bcbce6a Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Mon, 9 Mar 2026 09:12:39 +0200 Subject: [PATCH 08/14] Fix DRM capture breakage on screen blank, fix DRM handle leak * The screen capture now resumes automatically after a blank screen * Fixed leaking of framebuffer handle * Fixed spurious noisy logs on nvidia capture due to changing handles --- meshcore/KVM/Linux/linux_kvm.c | 4 + meshcore/KVM/Linux/linux_kvm_drm.c | 298 ++++++++++++++++++++++++++--- meshcore/agentcore.c | 2 + 3 files changed, 277 insertions(+), 27 deletions(-) diff --git a/meshcore/KVM/Linux/linux_kvm.c b/meshcore/KVM/Linux/linux_kvm.c index 847952a0e..c4b61dc36 100644 --- a/meshcore/KVM/Linux/linux_kvm.c +++ b/meshcore/KVM/Linux/linux_kvm.c @@ -1461,6 +1461,10 @@ void* kvm_relay_restart(int paused, void *processPipeMgr, ILibKVM_WriteHandler w int r; int count = 0; ILibProcessPipe_Pipe slave_out; + //printf("KVM: Restarting the KVM session for uid %d...\n", uid); + //if (authToken != NULL) { printf("KVM: Using XAuthToken: %s\n", authToken); } + //if (dispid != NULL) { printf("KVM: Using DisplayId: %s\n", dispid); } + //fflush(stdout); if (g_slavekvm != 0) { diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index 7cdce6254..1db4d2b90 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -237,6 +237,7 @@ typedef struct kvm_drm_frame_map uint8_t *addr; size_t size; int dma_fd; + int drm_fd; } kvm_drm_frame_map; typedef struct kvm_drm_scanout_frame @@ -251,6 +252,9 @@ typedef struct kvm_drm_scanout_frame uint64_t modifier; } kvm_drm_scanout_frame; +static uint32_t kvm_drm_get_plane_fb_id(int fd, uint32_t crtc_id, int crtc_index); +static uint32_t kvm_drm_get_scanout_fb_id(int fd, uint32_t crtc_id, int crtc_index, bool *out_have_crtc, bool *out_used_plane_fb); + static void kvm_drm_copy_error_message(char *dst, size_t dst_size, const char *src) { if (dst == NULL || dst_size == 0) @@ -310,6 +314,81 @@ static void kvm_drm_debug_log_scanout_frame(const char *prefix, const kvm_drm_sc frame->handle); } +static void kvm_drm_close_gem_handle(int fd, uint32_t handle) +{ + struct drm_gem_close closeReq; + + if (fd < 0 || handle == 0) + { + return; + } + + memset(&closeReq, 0, sizeof(closeReq)); + closeReq.handle = handle; + if (drmIoctl(fd, DRM_IOCTL_GEM_CLOSE, &closeReq) != 0 && drm_debug) + { + fprintf(stderr, "DRM: DRM_IOCTL_GEM_CLOSE failed for handle=%u (errno=%d)\n", handle, errno); + } +} + +static void kvm_drm_reset_logged_scanout_state(uint32_t *lastLoggedFbId, + uint32_t *lastLoggedWidth, + uint32_t *lastLoggedHeight, + uint32_t *lastLoggedPitch, + uint32_t *lastLoggedOffset, + uint32_t *lastLoggedFormat, + uint32_t *lastLoggedHandle, + uint64_t *lastLoggedModifier, + int *lastLoggedPath) +{ + if (lastLoggedFbId != NULL) { *lastLoggedFbId = 0; } + if (lastLoggedWidth != NULL) { *lastLoggedWidth = 0; } + if (lastLoggedHeight != NULL) { *lastLoggedHeight = 0; } + if (lastLoggedPitch != NULL) { *lastLoggedPitch = 0; } + if (lastLoggedOffset != NULL) { *lastLoggedOffset = 0; } + if (lastLoggedFormat != NULL) { *lastLoggedFormat = 0; } + if (lastLoggedHandle != NULL) { *lastLoggedHandle = 0; } + if (lastLoggedModifier != NULL) { *lastLoggedModifier = UINT64_MAX; } + if (lastLoggedPath != NULL) { *lastLoggedPath = -1; } +} + +static bool kvm_drm_same_output(const kvm_drm_output *a, const kvm_drm_output *b) +{ + if (a == NULL || b == NULL) + { + return false; + } + + return a->connector_id == b->connector_id && + a->crtc_id == b->crtc_id && + a->crtc_index == b->crtc_index && + strcmp(a->device_path, b->device_path) == 0; +} + +static bool kvm_drm_is_transient_scanout_error(const char *err) +{ + if (err == NULL) + { + return false; + } + + return strcmp(err, "Active CRTC has no framebuffer") == 0 || + strcmp(err, "drmModeGetCrtc failed") == 0; +} + +static bool kvm_drm_is_expected_suspended_refresh_error(const char *err) +{ + static const char *noOutputPrefix = "No connected display with active CRTC on "; + + if (err == NULL) + { + return false; + } + + return strcmp(err, "No active DRM framebuffer available yet") == 0 || + strncmp(err, noOutputPrefix, strlen(noOutputPrefix)) == 0; +} + static const char *kvm_drm_connector_type_name(uint32_t t) { switch (t) @@ -363,10 +442,15 @@ static void kvm_drm_destroy_map(kvm_drm_frame_map *map) { close(map->dma_fd); } + if (map->handle != 0) + { + kvm_drm_close_gem_handle(map->drm_fd, map->handle); + } map->handle = 0; map->addr = NULL; map->size = 0; map->dma_fd = -1; + map->drm_fd = -1; } static bool kvm_drm_map_framebuffer_handle(int fd, uint32_t handle, size_t min_size, kvm_drm_frame_map *map, char *out_error, size_t out_error_size) @@ -389,6 +473,7 @@ static bool kvm_drm_map_framebuffer_handle(int fd, uint32_t handle, size_t min_s map->handle = handle; map->addr = (uint8_t *)ptr; map->size = min_size; + map->drm_fd = fd; return true; } } @@ -403,6 +488,7 @@ static bool kvm_drm_map_framebuffer_handle(int fd, uint32_t handle, size_t min_s map->addr = (uint8_t *)ptr; map->size = min_size; map->dma_fd = dma_fd; + map->drm_fd = fd; return true; } close(dma_fd); @@ -462,7 +548,7 @@ static uint32_t kvm_drm_pick_crtc_for_connector(int fd, const drmModeRes *res, c return 0; } -static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_output *out, char *out_error, size_t out_error_size) +static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_output *out, bool logSelection, char *out_error, size_t out_error_size) { drmModeRes *res = drmModeGetResources(fd); if (res == NULL) @@ -472,6 +558,9 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o } bool found = false; + bool foundWithScanout = false; + kvm_drm_output fallback; + memset(&fallback, 0, sizeof(fallback)); int i; for (i = 0; i < res->count_connectors; ++i) { @@ -510,31 +599,95 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o continue; } - snprintf(out->device_path, sizeof(out->device_path), "%s", path); - snprintf(out->connector_name, sizeof(out->connector_name), "%s-%u", kvm_drm_connector_type_name(conn->connector_type), conn->connector_type_id); - out->connector_id = conn->connector_id; - out->crtc_id = crtc_id; - out->crtc_index = crtc_index; - if (drm_debug) + kvm_drm_output candidate; + memset(&candidate, 0, sizeof(candidate)); + snprintf(candidate.device_path, sizeof(candidate.device_path), "%s", path); + snprintf(candidate.connector_name, sizeof(candidate.connector_name), "%s-%u", kvm_drm_connector_type_name(conn->connector_type), conn->connector_type_id); + candidate.connector_id = conn->connector_id; + candidate.crtc_id = crtc_id; + candidate.crtc_index = crtc_index; + + if (!found) { - fprintf(stderr, "DRM: Selected output %s on %s (connector=%u crtc=%u index=%d)\n", - out->connector_name, out->device_path, out->connector_id, out->crtc_id, out->crtc_index); + fallback = candidate; + found = true; + } + + if (kvm_drm_get_scanout_fb_id(fd, crtc_id, crtc_index, NULL, NULL) != 0) + { + *out = candidate; + foundWithScanout = true; + drmModeFreeConnector(conn); + break; } - found = true; drmModeFreeConnector(conn); - break; } drmModeFreeResources(res); - if (!found) + if (foundWithScanout) + { + if (drm_debug && logSelection) + { + fprintf(stderr, "DRM: Selected output %s on %s (connector=%u crtc=%u index=%d)\n", + out->connector_name, out->device_path, out->connector_id, out->crtc_id, out->crtc_index); + } + return true; + } + + if (found) + { + *out = fallback; + if (drm_debug && logSelection) + { + fprintf(stderr, "DRM: Selected output %s on %s (connector=%u crtc=%u index=%d, waiting for scanout)\n", + out->connector_name, out->device_path, out->connector_id, out->crtc_id, out->crtc_index); + } + return true; + } + { char err[KVM_DRM_MAX_ERROR]; snprintf(err, sizeof(err), "No connected display with active CRTC on %s", path); kvm_drm_copy_error_message(out_error, out_error_size, err); } - return found; + return false; +} + +static bool kvm_drm_refresh_output_on_fd(int fd, kvm_drm_output *current, bool require_scanout, char *out_error, size_t out_error_size) +{ + kvm_drm_output refreshed; + memset(&refreshed, 0, sizeof(refreshed)); + if (current == NULL) + { + kvm_drm_copy_error_message(out_error, out_error_size, "Current DRM output is null"); + return false; + } + + if (!kvm_drm_find_active_output_on_fd(fd, current->device_path, &refreshed, false, out_error, out_error_size)) + { + return false; + } + + if (require_scanout && kvm_drm_get_scanout_fb_id(fd, refreshed.crtc_id, refreshed.crtc_index, NULL, NULL) == 0) + { + kvm_drm_copy_error_message(out_error, out_error_size, "No active DRM framebuffer available yet"); + return false; + } + + if (drm_debug && !kvm_drm_same_output(current, &refreshed)) + { + fprintf(stderr, "DRM: Switched output from %s/%u to %s/%u on %s\n", + current->connector_name, + current->crtc_id, + refreshed.connector_name, + refreshed.crtc_id, + refreshed.device_path); + } + + *current = refreshed; + return true; } static bool kvm_drm_open_device_with_output(const char *explicit_device, int *out_fd, kvm_drm_output *out, char *out_error, size_t out_error_size) @@ -550,7 +703,7 @@ static bool kvm_drm_open_device_with_output(const char *explicit_device, int *ou kvm_drm_copy_error_message(out_error, out_error_size, err); return false; } - if (kvm_drm_find_active_output_on_fd(fd, explicit_device, out, out_error, out_error_size)) + if (kvm_drm_find_active_output_on_fd(fd, explicit_device, out, true, out_error, out_error_size)) { *out_fd = fd; return true; @@ -568,7 +721,7 @@ static bool kvm_drm_open_device_with_output(const char *explicit_device, int *ou { continue; } - if (kvm_drm_find_active_output_on_fd(fd, path, out, out_error, out_error_size)) + if (kvm_drm_find_active_output_on_fd(fd, path, out, true, out_error, out_error_size)) { *out_fd = fd; return true; @@ -636,31 +789,56 @@ static uint32_t kvm_drm_get_plane_fb_id(int fd, uint32_t crtc_id, int crtc_index return best_fb_id; } -static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, kvm_drm_scanout_frame *out, char *out_error, size_t out_error_size) +static uint32_t kvm_drm_get_scanout_fb_id(int fd, uint32_t crtc_id, int crtc_index, bool *out_have_crtc, bool *out_used_plane_fb) { drmModeCrtc *crtc = drmModeGetCrtc(fd, crtc_id); + uint32_t fb_id = 0; + + if (out_have_crtc != NULL) { *out_have_crtc = false; } + if (out_used_plane_fb != NULL) { *out_used_plane_fb = false; } + if (crtc == NULL) { - kvm_drm_copy_error_message(out_error, out_error_size, "drmModeGetCrtc failed"); - return false; + return 0; } - uint32_t fb_id = crtc->buffer_id; + if (out_have_crtc != NULL) { *out_have_crtc = true; } + + fb_id = crtc->buffer_id; drmModeFreeCrtc(crtc); if (fb_id == 0) { fb_id = kvm_drm_get_plane_fb_id(fd, crtc_id, crtc_index); - if (drm_debug && fb_id != 0) + if (fb_id != 0 && out_used_plane_fb != NULL) { - fprintf(stderr, "DRM: CRTC %u has no direct buffer_id, using plane framebuffer %u\n", crtc_id, fb_id); + *out_used_plane_fb = true; } } + + return fb_id; +} + +static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, kvm_drm_scanout_frame *out, char *out_error, size_t out_error_size) +{ + bool have_crtc = false; + bool used_plane_fb = false; + uint32_t fb_id = kvm_drm_get_scanout_fb_id(fd, crtc_id, crtc_index, &have_crtc, &used_plane_fb); + + if (!have_crtc) + { + kvm_drm_copy_error_message(out_error, out_error_size, "drmModeGetCrtc failed"); + return false; + } if (fb_id == 0) { kvm_drm_copy_error_message(out_error, out_error_size, "Active CRTC has no framebuffer"); return false; } + if (drm_debug && used_plane_fb) + { + fprintf(stderr, "DRM: CRTC %u has no direct buffer_id, using plane framebuffer %u\n", crtc_id, fb_id); + } bool have_fb2_meta = false; drmModeFB2 *fb2 = drmModeGetFB2(fd, fb_id); @@ -673,7 +851,7 @@ static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, ++planeCount; } - if (drm_debug) + if (drm_debug >= 2) { char format[32]; kvm_drm_format_fourcc(format, sizeof(format), fb2->pixel_format); @@ -1186,6 +1364,11 @@ void *kvm_server_mainloop_drm(void *parm) uint64_t lastFrameTimeMs = 0; int lastCaptureError = 0; int64_t nFramesConverted = 0; + int scanoutSuspended = 0; + int forceTileReset = 0; + uint64_t lastOutputRefreshMs = 0; + uint64_t lastRefreshFailureLogMs = 0; + char lastRefreshFailure[KVM_DRM_MAX_ERROR]; g_kvmBackendDRM = 1; g_enableEvents = kvm_events_evdev_init(); @@ -1236,7 +1419,9 @@ void *kvm_server_mainloop_drm(void *parm) memset(&output, 0, sizeof(output)); memset(&map, 0, sizeof(map)); memset(&eglCtx, 0, sizeof(eglCtx)); + memset(lastRefreshFailure, 0, sizeof(lastRefreshFailure)); map.dma_fd = -1; + map.drm_fd = -1; char *explicitDevice = getenv("MESH_KVM_DRM_DEVICE"); if (!kvm_drm_open_device_with_output(explicitDevice, &fd, &output, err, sizeof(err))) @@ -1343,6 +1528,47 @@ void *kvm_server_mainloop_drm(void *parm) if (!kvm_drm_get_scanout_frame(fd, output.crtc_id, output.crtc_index, &frame, err, sizeof(err))) { + if (kvm_drm_is_transient_scanout_error(err)) + { + if (!scanoutSuspended) + { + scanoutSuspended = 1; + forceTileReset = 1; + kvm_drm_destroy_map(&map); + kvm_drm_egl_destroy_context(&eglCtx); + kvm_drm_reset_logged_scanout_state(&lastLoggedFbId, &lastLoggedWidth, &lastLoggedHeight, + &lastLoggedPitch, &lastLoggedOffset, &lastLoggedFormat, &lastLoggedHandle, + &lastLoggedModifier, &lastLoggedPath); + if (drm_debug) + { + fprintf(stderr, "DRM: Scanout unavailable on %s, waiting for the display to resume\n", output.connector_name); + } + lastRefreshFailure[0] = 0; + lastRefreshFailureLogMs = 0; + } + + if (lastOutputRefreshMs == 0 || nowMs - lastOutputRefreshMs >= 250) + { + char refreshErr[KVM_DRM_MAX_ERROR]; + if (kvm_drm_refresh_output_on_fd(fd, &output, true, refreshErr, sizeof(refreshErr))) + { + lastRefreshFailure[0] = 0; + lastRefreshFailureLogMs = 0; + } + else if (drm_debug && !kvm_drm_is_expected_suspended_refresh_error(refreshErr) && + (lastRefreshFailure[0] == 0 || + strcmp(lastRefreshFailure, refreshErr) != 0 || + nowMs - lastRefreshFailureLogMs >= 5000)) + { + fprintf(stderr, "DRM: Output refresh while scanout is suspended failed: %s\n", refreshErr); + kvm_drm_copy_error_message(lastRefreshFailure, sizeof(lastRefreshFailure), refreshErr); + lastRefreshFailureLogMs = nowMs; + } + lastOutputRefreshMs = nowMs; + } + continue; + } + if (lastCaptureError == 0) { kvm_send_error(err); @@ -1351,6 +1577,18 @@ void *kvm_server_mainloop_drm(void *parm) continue; } + if (scanoutSuspended) + { + scanoutSuspended = 0; + lastOutputRefreshMs = 0; + lastRefreshFailure[0] = 0; + lastRefreshFailureLogMs = 0; + if (drm_debug) + { + fprintf(stderr, "DRM: Scanout resumed on %s\n", output.connector_name); + } + } + if (frame.handle == 0 || frame.width == 0 || frame.height == 0) { if (lastCaptureError == 0) @@ -1362,23 +1600,19 @@ void *kvm_server_mainloop_drm(void *parm) } if (drm_debug && - (frame.fb_id != lastLoggedFbId || - frame.width != lastLoggedWidth || + (frame.width != lastLoggedWidth || frame.height != lastLoggedHeight || frame.pitch != lastLoggedPitch || frame.offset != lastLoggedOffset || frame.format != lastLoggedFormat || - frame.handle != lastLoggedHandle || frame.modifier != lastLoggedModifier)) { kvm_drm_debug_log_scanout_frame("Using scanout framebuffer", &frame); - lastLoggedFbId = frame.fb_id; lastLoggedWidth = frame.width; lastLoggedHeight = frame.height; lastLoggedPitch = frame.pitch; lastLoggedOffset = frame.offset; lastLoggedFormat = frame.format; - lastLoggedHandle = frame.handle; lastLoggedModifier = frame.modifier; } @@ -1400,6 +1634,7 @@ void *kvm_server_mainloop_drm(void *parm) kvm_send_resolution(); kvm_send_display(); reset_tile_info(oldTileHeightCount); + forceTileReset = 0; } if (!displayListSent) @@ -1429,12 +1664,15 @@ void *kvm_server_mainloop_drm(void *parm) converted = kvm_drm_egl_convert_to_rgb24_gpu(&eglCtx, fd, frame.width, frame.height, frame.pitch, frame.offset, frame.format, frame.handle, frame.modifier, rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); + kvm_drm_close_gem_handle(fd, frame.handle); + frame.handle = 0; } else { size_t required_bytes = (size_t)frame.offset + ((size_t)frame.pitch * (size_t)frame.height); if (!kvm_drm_map_framebuffer_handle(fd, frame.handle, required_bytes, &map, err, sizeof(err))) { + kvm_drm_close_gem_handle(fd, frame.handle); converted = false; } else @@ -1467,6 +1705,12 @@ void *kvm_server_mainloop_drm(void *parm) if (g_tileInfo == NULL) { reset_tile_info(0); + forceTileReset = 0; + } + else if (forceTileReset) + { + reset_tile_info(TILE_HEIGHT_COUNT); + forceTileReset = 0; } if (kvm_drm_send_dirty_tiles(rgbBuffer, rgbSize, &desktopBuffer, &desktopBufferSize) != 0) diff --git a/meshcore/agentcore.c b/meshcore/agentcore.c index f330e8ee6..6564f2468 100644 --- a/meshcore/agentcore.c +++ b/meshcore/agentcore.c @@ -1353,6 +1353,8 @@ duk_ret_t ILibDuktape_MeshAgent_getRemoteDesktop(duk_context *ctx) char *updateXAuth = NULL; char *updateDisplay = NULL; int waylandSession = kvm_is_wayland_session_for_uid(console_uid); + //printf("ILibDuktape_MeshAgent_getRemoteDesktop: waylandSession = %d, console_uid = %d\n", waylandSession, console_uid); + //fflush(stdout); int needPop = 0; duk_eval_string(ctx, "require('user-sessions').Self()"); int self = duk_get_int(ctx, -1); duk_pop(ctx); From 4726a8b43fe1e6693a7ac260c25f692701907c90 Mon Sep 17 00:00:00 2001 From: Ben Harper Date: Mon, 9 Mar 2026 13:11:50 +0200 Subject: [PATCH 09/14] Add rudimentary support for rotated wayland displays Unfortunately I haven't yet found a reliable universal way to detect display rotation on wayland, so I'm adding an env var override, eg For "portrait right": MESH_KVM_ROTATION=270 By setting MESH_KVM_ROTATION to anything besides zero, we force the treatment of the display as rotated by either 90,180,270 degrees. I split it into it's own .c file, because I forsee this getting a bit messy, having to support various compositors. --- makefile | 3 +- meshcore/KVM/Linux/linux_kvm_drm.c | 125 +++++++++++++----- meshcore/KVM/Linux/linux_kvm_rotated.c | 170 +++++++++++++++++++++++++ meshcore/KVM/Linux/linux_kvm_rotated.h | 34 +++++ 4 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 meshcore/KVM/Linux/linux_kvm_rotated.c create mode 100644 meshcore/KVM/Linux/linux_kvm_rotated.h diff --git a/makefile b/makefile index 9c4a588bd..62de1d145 100644 --- a/makefile +++ b/makefile @@ -541,7 +541,7 @@ endif ifeq ($(KVM),1) # Mesh Agent KVM, this is only included in builds that have KVM support -LINUXKVMSOURCES = meshcore/KVM/Linux/linux_kvm.c meshcore/KVM/Linux/linux_kvm_wayland.c meshcore/KVM/Linux/linux_kvm_drm.c meshcore/KVM/Linux/linux_kvm_drm_egl.c meshcore/KVM/Linux/linux_events.c meshcore/KVM/Linux/linux_events_evdev.c meshcore/KVM/Linux/linux_tile.c meshcore/KVM/Linux/linux_compression.c +LINUXKVMSOURCES = meshcore/KVM/Linux/linux_kvm.c meshcore/KVM/Linux/linux_kvm_wayland.c meshcore/KVM/Linux/linux_kvm_drm.c meshcore/KVM/Linux/linux_kvm_drm_egl.c meshcore/KVM/Linux/linux_kvm_rotated.c meshcore/KVM/Linux/linux_events.c meshcore/KVM/Linux/linux_events_evdev.c meshcore/KVM/Linux/linux_tile.c meshcore/KVM/Linux/linux_compression.c MACOSKVMSOURCES = meshcore/KVM/MacOS/mac_kvm.c meshcore/KVM/MacOS/mac_events.c meshcore/KVM/MacOS/mac_tile.c meshcore/KVM/Linux/linux_compression.c CFLAGS += -D_LINKVM DRMCFLAGS = $(shell pkg-config --cflags libdrm egl glesv2 2>/dev/null) @@ -812,4 +812,3 @@ openbsd: $(MAKE) EXENAME="$(EXENAME)_$(ARCHNAME)$(EXENAME2)" ADDITIONALSOURCES="$(LINUXKVMSOURCES)" AID="$(ARCHID)" CFLAGS="-std=gnu99 -Wall -DJPEGMAXBUF=$(KVMMaxTile) -DMESH_AGENTID=$(ARCHID) -D_POSIX -D_FREEBSD -D_OPENBSD -DILIB_NO_TIMEDJOIN -D_NOHECI -D_NOILIBSTACKDEBUG -DMICROSTACK_PROXY -fno-strict-aliasing $(INCDIRS) $(CFLAGS) $(CEXTRA)" LDFLAGS="$(BSDSSL) $(BSDFLAGS) -L. -lpthread -lz -lutil $(LDFLAGS) $(LDEXTRA)" $(SYMBOLCP) $(STRIP) - diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index 1db4d2b90..e90b8c2a8 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -17,6 +17,7 @@ limitations under the License. #include "linux_kvm_drm.h" #include "linux_kvm_drm_egl.h" #include "linux_kvm.h" +#include "linux_kvm_rotated.h" #include "linux_compression.h" #include "linux_tile.h" #include "meshcore/meshdefines.h" @@ -250,6 +251,7 @@ typedef struct kvm_drm_scanout_frame uint32_t format; uint32_t handle; uint64_t modifier; + kvm_drm_rotation rotation; } kvm_drm_scanout_frame; static uint32_t kvm_drm_get_plane_fb_id(int fd, uint32_t crtc_id, int crtc_index); @@ -302,7 +304,7 @@ static void kvm_drm_debug_log_scanout_frame(const char *prefix, const kvm_drm_sc kvm_drm_format_fourcc(format, sizeof(format), frame->format); fprintf(stderr, - "DRM: %s fb_id=%u size=%ux%u pitch=%u offset=%u format=%s modifier=0x%016" PRIx64 " handle=%u\n", + "DRM: %s fb_id=%u size=%ux%u pitch=%u offset=%u format=%s modifier=0x%016" PRIx64 " handle=%u rotation=%s\n", (prefix != NULL) ? prefix : "scanout", frame->fb_id, frame->width, @@ -311,7 +313,8 @@ static void kvm_drm_debug_log_scanout_frame(const char *prefix, const kvm_drm_sc frame->offset, format, frame->modifier, - frame->handle); + frame->handle, + kvm_drm_rotation_name(frame->rotation)); } static void kvm_drm_close_gem_handle(int fd, uint32_t handle) @@ -693,6 +696,7 @@ static bool kvm_drm_refresh_output_on_fd(int fd, kvm_drm_output *current, bool r static bool kvm_drm_open_device_with_output(const char *explicit_device, int *out_fd, kvm_drm_output *out, char *out_error, size_t out_error_size) { int i; + if (explicit_device != NULL && explicit_device[0] != 0) { int fd = open(explicit_device, O_RDWR | KVM_DRM_O_CLOEXEC | O_NONBLOCK); @@ -789,6 +793,16 @@ static uint32_t kvm_drm_get_plane_fb_id(int fd, uint32_t crtc_id, int crtc_index return best_fb_id; } +static kvm_drm_rotation kvm_drm_get_scanout_rotation() +{ + kvm_drm_rotation forced = KVM_DRM_ROTATION_0; + if (kvm_drm_get_forced_rotation(&forced, drm_debug)) + { + return forced; + } + return KVM_DRM_ROTATION_0; +} + static uint32_t kvm_drm_get_scanout_fb_id(int fd, uint32_t crtc_id, int crtc_index, bool *out_have_crtc, bool *out_used_plane_fb) { drmModeCrtc *crtc = drmModeGetCrtc(fd, crtc_id); @@ -825,6 +839,8 @@ static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, bool used_plane_fb = false; uint32_t fb_id = kvm_drm_get_scanout_fb_id(fd, crtc_id, crtc_index, &have_crtc, &used_plane_fb); + out->rotation = KVM_DRM_ROTATION_0; + if (!have_crtc) { kvm_drm_copy_error_message(out_error, out_error_size, "drmModeGetCrtc failed"); @@ -894,6 +910,7 @@ static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, { out->fb_id = fb_id; out->handle = fb2->handles[0]; + out->rotation = kvm_drm_get_scanout_rotation(); drmModeFreeFB2(fb2); return true; } @@ -920,6 +937,7 @@ static bool kvm_drm_get_scanout_frame(int fd, uint32_t crtc_id, int crtc_index, out->modifier = DRM_FORMAT_MOD_LINEAR; } out->handle = fb->handle; + out->rotation = kvm_drm_get_scanout_rotation(); drmModeFreeFB(fb); return true; } @@ -1251,6 +1269,17 @@ static bool kvm_drm_convert_to_rgb24(const kvm_drm_scanout_frame *f, const uint8 return true; } +static void kvm_drm_get_rotated_dimensions(const kvm_drm_scanout_frame *frame, uint32_t *out_width, uint32_t *out_height) +{ + *out_width = frame->width; + *out_height = frame->height; + if (frame->rotation == KVM_DRM_ROTATION_90 || frame->rotation == KVM_DRM_ROTATION_270) + { + *out_width = frame->height; + *out_height = frame->width; + } +} + static uint64_t kvm_drm_now_ms() { struct timespec tsNow; @@ -1405,6 +1434,8 @@ void *kvm_server_mainloop_drm(void *parm) kvm_drm_egl_context eglCtx; unsigned char *rgbBuffer = NULL; size_t rgbBufferSize = 0; + unsigned char *rgbRotatedBuffer = NULL; + size_t rgbRotatedBufferSize = 0; char *desktopBuffer = NULL; long long desktopBufferSize = 0; uint32_t lastLoggedFbId = 0; @@ -1416,6 +1447,7 @@ void *kvm_server_mainloop_drm(void *parm) uint32_t lastLoggedHandle = 0; uint64_t lastLoggedModifier = UINT64_MAX; int lastLoggedPath = -1; + int lastLoggedRotation = -1; memset(&output, 0, sizeof(output)); memset(&map, 0, sizeof(map)); memset(&eglCtx, 0, sizeof(eglCtx)); @@ -1539,6 +1571,7 @@ void *kvm_server_mainloop_drm(void *parm) kvm_drm_reset_logged_scanout_state(&lastLoggedFbId, &lastLoggedWidth, &lastLoggedHeight, &lastLoggedPitch, &lastLoggedOffset, &lastLoggedFormat, &lastLoggedHandle, &lastLoggedModifier, &lastLoggedPath); + lastLoggedRotation = -1; if (drm_debug) { fprintf(stderr, "DRM: Scanout unavailable on %s, waiting for the display to resume\n", output.connector_name); @@ -1605,7 +1638,8 @@ void *kvm_server_mainloop_drm(void *parm) frame.pitch != lastLoggedPitch || frame.offset != lastLoggedOffset || frame.format != lastLoggedFormat || - frame.modifier != lastLoggedModifier)) + frame.modifier != lastLoggedModifier || + (int)frame.rotation != lastLoggedRotation)) { kvm_drm_debug_log_scanout_frame("Using scanout framebuffer", &frame); lastLoggedWidth = frame.width; @@ -1614,27 +1648,33 @@ void *kvm_server_mainloop_drm(void *parm) lastLoggedOffset = frame.offset; lastLoggedFormat = frame.format; lastLoggedModifier = frame.modifier; + lastLoggedRotation = (int)frame.rotation; } - if (SCREEN_WIDTH != (int)frame.width || SCREEN_HEIGHT != (int)frame.height) { - int oldTileHeightCount = TILE_HEIGHT_COUNT; - SCREEN_WIDTH = (int)frame.width; - SCREEN_HEIGHT = (int)frame.height; - TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; - TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; - if (SCREEN_WIDTH % TILE_WIDTH) + uint32_t effectiveWidth = 0; + uint32_t effectiveHeight = 0; + kvm_drm_get_rotated_dimensions(&frame, &effectiveWidth, &effectiveHeight); + if (SCREEN_WIDTH != (int)effectiveWidth || SCREEN_HEIGHT != (int)effectiveHeight) { - TILE_WIDTH_COUNT++; - } - if (SCREEN_HEIGHT % TILE_HEIGHT) - { - TILE_HEIGHT_COUNT++; + int oldTileHeightCount = TILE_HEIGHT_COUNT; + SCREEN_WIDTH = (int)effectiveWidth; + SCREEN_HEIGHT = (int)effectiveHeight; + TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; + TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; + if (SCREEN_WIDTH % TILE_WIDTH) + { + TILE_WIDTH_COUNT++; + } + if (SCREEN_HEIGHT % TILE_HEIGHT) + { + TILE_HEIGHT_COUNT++; + } + kvm_send_resolution(); + kvm_send_display(); + reset_tile_info(oldTileHeightCount); + forceTileReset = 0; } - kvm_send_resolution(); - kvm_send_display(); - reset_tile_info(oldTileHeightCount); - forceTileReset = 0; } if (!displayListSent) @@ -1702,20 +1742,38 @@ void *kvm_server_mainloop_drm(void *parm) lastCaptureError = 0; nFramesConverted++; - if (g_tileInfo == NULL) - { - reset_tile_info(0); - forceTileReset = 0; - } - else if (forceTileReset) { - reset_tile_info(TILE_HEIGHT_COUNT); - forceTileReset = 0; - } + const unsigned char *rgbFrame = rgbBuffer; + size_t rgbFrameSize = rgbSize; - if (kvm_drm_send_dirty_tiles(rgbBuffer, rgbSize, &desktopBuffer, &desktopBufferSize) != 0) - { - g_shutdown = 1; + if (frame.rotation != KVM_DRM_ROTATION_0) + { + if (rgbRotatedBufferSize < rgbSize) + { + unsigned char *tmp = (unsigned char *)realloc(rgbRotatedBuffer, rgbSize); + if (tmp == NULL) ILIBCRITICALEXIT(254); + rgbRotatedBuffer = tmp; + rgbRotatedBufferSize = rgbSize; + } + kvm_drm_rotate_rgb24(rgbBuffer, frame.width, frame.height, frame.rotation, rgbRotatedBuffer); + rgbFrame = rgbRotatedBuffer; + } + + if (g_tileInfo == NULL) + { + reset_tile_info(0); + forceTileReset = 0; + } + else if (forceTileReset) + { + reset_tile_info(TILE_HEIGHT_COUNT); + forceTileReset = 0; + } + + if (kvm_drm_send_dirty_tiles(rgbFrame, rgbFrameSize, &desktopBuffer, &desktopBufferSize) != 0) + { + g_shutdown = 1; + } } } @@ -1737,6 +1795,11 @@ void *kvm_server_mainloop_drm(void *parm) free(rgbBuffer); rgbBuffer = NULL; } + if (rgbRotatedBuffer != NULL) + { + free(rgbRotatedBuffer); + rgbRotatedBuffer = NULL; + } kvm_drm_destroy_map(&map); kvm_drm_egl_destroy_context(&eglCtx); if (fd >= 0) diff --git a/meshcore/KVM/Linux/linux_kvm_rotated.c b/meshcore/KVM/Linux/linux_kvm_rotated.c new file mode 100644 index 000000000..71d1cb92b --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_rotated.c @@ -0,0 +1,170 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Detecting rotation on Wayland turns out to be really platform-specific. +// I tried first reading the data via the official DRM APIs, but that didn't +// work on a Raspberry Pi or a qemu VM, so I gave up on that. It seems like +// the only practical solution is to support the various Wayland compositors, +// and read their state out directly. +// So right now the only way to get screen rotation to work on Wayland +// is to specify it via an env var, with 0,90,180,270, eg: +// For portrait-right: +// MESH_KVM_ROTATION=270 + +#include "linux_kvm_rotated.h" + +#include +#include +#include + +extern void kvm_send_error(char *msg); + +const char *kvm_drm_rotation_name(kvm_drm_rotation rotation) +{ + switch (rotation) + { + case KVM_DRM_ROTATION_90: + return "90"; + case KVM_DRM_ROTATION_180: + return "180"; + case KVM_DRM_ROTATION_270: + return "270"; + case KVM_DRM_ROTATION_0: + default: + return "0"; + } +} + +int kvm_drm_get_forced_rotation(kvm_drm_rotation *out_rotation, int log_enabled) +{ + static int initialized = 0; + static int have_forced = 0; + static kvm_drm_rotation forced_rotation = KVM_DRM_ROTATION_0; + + if (!initialized) + { + const char *value = getenv("MESH_KVM_ROTATION"); + static kvm_drm_rotation fromenv = (kvm_drm_rotation) -1; + initialized = 1; + if (value != NULL && value[0] != 0) + { + if (strcmp(value, "0") == 0) + { + fromenv = KVM_DRM_ROTATION_0; + } + else if (strcmp(value, "90") == 0) + { + fromenv = KVM_DRM_ROTATION_90; + } + else if (strcmp(value, "180") == 0) + { + fromenv = KVM_DRM_ROTATION_180; + } + else if (strcmp(value, "270") == 0) + { + fromenv = KVM_DRM_ROTATION_270; + } + + if (fromenv != (kvm_drm_rotation) -1) + { + have_forced = 1; + forced_rotation = fromenv; + if (log_enabled) + { + fprintf(stderr, "DRM: forced rotation enabled via MESH_KVM_ROTATION=%s\n", value); + } + } + else if (log_enabled) + { + fprintf(stderr, "DRM: invalid MESH_KVM_ROTATION value '%s' (expected 0/90/180/270)\n", value); + } + } + } + + if (!have_forced) + { + return 0; + } + + *out_rotation = forced_rotation; + return 1; +} + +void kvm_drm_rotate_rgb24(const uint8_t *src, uint32_t src_width, uint32_t src_height, kvm_drm_rotation rotation, uint8_t *dst) +{ + size_t src_stride = ((size_t)src_width) * 3u; + size_t dst_stride = ((size_t)((rotation == KVM_DRM_ROTATION_90 || rotation == KVM_DRM_ROTATION_270) ? src_height : src_width)) * 3u; + uint32_t x; + uint32_t y; + + if (src == NULL || dst == NULL) + { + kvm_send_error("Invalid RGB rotation buffer"); + return; + } + + switch (rotation) + { + case KVM_DRM_ROTATION_90: + for (y = 0; y < src_height; ++y) + { + const uint8_t *s = src + (((size_t)y) * src_stride); + uint8_t *d = dst + (((size_t)(src_width - 1u)) * dst_stride) + (((size_t)y) * 3u); + for (x = 0; x < src_width; ++x) + { + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + s += 3; + d -= dst_stride; + } + } + break; + case KVM_DRM_ROTATION_180: + for (y = 0; y < src_height; ++y) + { + const uint8_t *s = src + (((size_t)y) * src_stride); + uint8_t *d = dst + (((size_t)(src_height - 1u - y)) * dst_stride) + (((size_t)(src_width - 1u)) * 3u); + for (x = 0; x < src_width; ++x) + { + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + s += 3; + d -= 3; + } + } + break; + case KVM_DRM_ROTATION_270: + for (y = 0; y < src_height; ++y) + { + const uint8_t *s = src + (((size_t)y) * src_stride); + uint8_t *d = dst + (((size_t)(src_height - 1u - y)) * 3u); + for (x = 0; x < src_width; ++x) + { + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + s += 3; + d += dst_stride; + } + } + break; + case KVM_DRM_ROTATION_0: + default: + break; + } +} diff --git a/meshcore/KVM/Linux/linux_kvm_rotated.h b/meshcore/KVM/Linux/linux_kvm_rotated.h new file mode 100644 index 000000000..a60ceff57 --- /dev/null +++ b/meshcore/KVM/Linux/linux_kvm_rotated.h @@ -0,0 +1,34 @@ +/* +Copyright 2010 - 2011 Intel Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#ifndef LINUX_KVM_ROTATED_H_ +#define LINUX_KVM_ROTATED_H_ + +#include + +typedef enum kvm_drm_rotation +{ + KVM_DRM_ROTATION_0 = 0, + KVM_DRM_ROTATION_90 = 1, + KVM_DRM_ROTATION_180 = 2, + KVM_DRM_ROTATION_270 = 3 +} kvm_drm_rotation; + +const char *kvm_drm_rotation_name(kvm_drm_rotation rotation); +int kvm_drm_get_forced_rotation(kvm_drm_rotation *out_rotation, int log_enabled); +void kvm_drm_rotate_rgb24(const uint8_t *src, uint32_t src_width, uint32_t src_height, kvm_drm_rotation rotation, uint8_t *dst); + +#endif From 941862de2145d6dcef8f2405dc44d1932fbb08a1 Mon Sep 17 00:00:00 2001 From: Patrick O'Connell Date: Mon, 25 May 2026 15:56:10 +1000 Subject: [PATCH 10/14] Improve DRM Wayland KVM multi-monitor layout handling Add multi-output DRM capture support and compose active scanout buffers into a single logical desktop frame. When running under KWin, read the session's logical output geometry and apply it to the DRM outputs so capture and input coordinates follow the desktop monitor arrangement, including scaled displays. Also make verbose DRM/EGL diagnostics opt-in through MESH_KVM_DRM_DEBUG to avoid repeated framebuffer logging during normal agent operation. --- meshcore/KVM/Linux/linux_kvm_drm.c | 823 +++++++++++++++++-------- meshcore/KVM/Linux/linux_kvm_drm_egl.c | 13 +- 2 files changed, 578 insertions(+), 258 deletions(-) diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index e90b8c2a8..da0c475a3 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -58,9 +58,10 @@ limitations under the License. #endif #define KVM_DRM_MAX_ERROR 256 +#define KVM_DRM_MAX_OUTPUTS 16 int g_kvmBackendDRM = 0; -static int drm_debug = 1; +static int drm_debug = 0; extern int SCREEN_NUM; extern int SCREEN_WIDTH; @@ -230,8 +231,22 @@ typedef struct kvm_drm_output uint32_t connector_id; uint32_t crtc_id; int crtc_index; + int x; + int y; + uint32_t width; + uint32_t height; } kvm_drm_output; +typedef struct kvm_drm_desktop_layout +{ + int min_x; + int min_y; + int max_x; + int max_y; + uint32_t width; + uint32_t height; +} kvm_drm_desktop_layout; + typedef struct kvm_drm_frame_map { uint32_t handle; @@ -274,6 +289,18 @@ static void kvm_drm_copy_error_message(char *dst, size_t dst_size, const char *s dst[n] = 0; } +static void kvm_drm_init_debug() +{ + const char *value = getenv("MESH_KVM_DRM_DEBUG"); + if (value == NULL || value[0] == 0) + { + drm_debug = 0; + return; + } + drm_debug = atoi(value); + if (drm_debug < 0) { drm_debug = 0; } +} + static void kvm_drm_format_fourcc(char *dst, size_t dst_size, uint32_t format) { char code[5]; @@ -355,19 +382,6 @@ static void kvm_drm_reset_logged_scanout_state(uint32_t *lastLoggedFbId, if (lastLoggedPath != NULL) { *lastLoggedPath = -1; } } -static bool kvm_drm_same_output(const kvm_drm_output *a, const kvm_drm_output *b) -{ - if (a == NULL || b == NULL) - { - return false; - } - - return a->connector_id == b->connector_id && - a->crtc_id == b->crtc_id && - a->crtc_index == b->crtc_index && - strcmp(a->device_path, b->device_path) == 0; -} - static bool kvm_drm_is_transient_scanout_error(const char *err) { if (err == NULL) @@ -551,7 +565,30 @@ static uint32_t kvm_drm_pick_crtc_for_connector(int fd, const drmModeRes *res, c return 0; } -static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_output *out, bool logSelection, char *out_error, size_t out_error_size) +static int kvm_drm_output_index_by_crtc(kvm_drm_output *outputs, int output_count, uint32_t crtc_id) +{ + int i; + for (i = 0; i < output_count; ++i) + { + if (outputs[i].crtc_id == crtc_id) + { + return i; + } + } + return -1; +} + +static int kvm_drm_compare_outputs(const void *a, const void *b) +{ + const kvm_drm_output *oa = (const kvm_drm_output *)a; + const kvm_drm_output *ob = (const kvm_drm_output *)b; + if (oa->y != ob->y) { return oa->y < ob->y ? -1 : 1; } + if (oa->x != ob->x) { return oa->x < ob->x ? -1 : 1; } + if (oa->connector_id != ob->connector_id) { return oa->connector_id < ob->connector_id ? -1 : 1; } + return 0; +} + +static bool kvm_drm_collect_active_outputs_on_fd(int fd, const char *path, kvm_drm_output *outputs, int max_outputs, int *out_count, bool require_scanout, bool logSelection, char *out_error, size_t out_error_size) { drmModeRes *res = drmModeGetResources(fd); if (res == NULL) @@ -560,12 +597,10 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o return false; } - bool found = false; - bool foundWithScanout = false; - kvm_drm_output fallback; - memset(&fallback, 0, sizeof(fallback)); + int count = 0; + int connected = 0; int i; - for (i = 0; i < res->count_connectors; ++i) + for (i = 0; i < res->count_connectors && count < max_outputs; ++i) { drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[i]); if (conn == NULL) @@ -578,6 +613,7 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o drmModeFreeConnector(conn); continue; } + connected++; uint32_t crtc_id = kvm_drm_pick_crtc_for_connector(fd, res, conn); if (crtc_id == 0) @@ -596,12 +632,27 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o break; } } - if (crtc_index < 0) + if (crtc_index < 0 || kvm_drm_output_index_by_crtc(outputs, count, crtc_id) >= 0) + { + drmModeFreeConnector(conn); + continue; + } + + drmModeCrtc *crtc = drmModeGetCrtc(fd, crtc_id); + if (crtc == NULL) { drmModeFreeConnector(conn); continue; } + uint32_t fb_id = kvm_drm_get_scanout_fb_id(fd, crtc_id, crtc_index, NULL, NULL); + if (require_scanout && fb_id == 0) + { + drmModeFreeCrtc(crtc); + drmModeFreeConnector(conn); + continue; + } + kvm_drm_output candidate; memset(&candidate, 0, sizeof(candidate)); snprintf(candidate.device_path, sizeof(candidate.device_path), "%s", path); @@ -609,91 +660,45 @@ static bool kvm_drm_find_active_output_on_fd(int fd, const char *path, kvm_drm_o candidate.connector_id = conn->connector_id; candidate.crtc_id = crtc_id; candidate.crtc_index = crtc_index; + candidate.x = crtc->x; + candidate.y = crtc->y; + candidate.width = crtc->width; + candidate.height = crtc->height; + outputs[count++] = candidate; - if (!found) - { - fallback = candidate; - found = true; - } - - if (kvm_drm_get_scanout_fb_id(fd, crtc_id, crtc_index, NULL, NULL) != 0) - { - *out = candidate; - foundWithScanout = true; - drmModeFreeConnector(conn); - break; - } - + drmModeFreeCrtc(crtc); drmModeFreeConnector(conn); } drmModeFreeResources(res); - if (foundWithScanout) - { - if (drm_debug && logSelection) - { - fprintf(stderr, "DRM: Selected output %s on %s (connector=%u crtc=%u index=%d)\n", - out->connector_name, out->device_path, out->connector_id, out->crtc_id, out->crtc_index); - } - return true; - } - - if (found) - { - *out = fallback; - if (drm_debug && logSelection) - { - fprintf(stderr, "DRM: Selected output %s on %s (connector=%u crtc=%u index=%d, waiting for scanout)\n", - out->connector_name, out->device_path, out->connector_id, out->crtc_id, out->crtc_index); - } - return true; - } - + if (count <= 0) { char err[KVM_DRM_MAX_ERROR]; - snprintf(err, sizeof(err), "No connected display with active CRTC on %s", path); + snprintf(err, sizeof(err), "%s on %s", + connected > 0 ? "No connected display with active scanout" : "No connected display with active CRTC", + path); kvm_drm_copy_error_message(out_error, out_error_size, err); - } - return false; -} - -static bool kvm_drm_refresh_output_on_fd(int fd, kvm_drm_output *current, bool require_scanout, char *out_error, size_t out_error_size) -{ - kvm_drm_output refreshed; - memset(&refreshed, 0, sizeof(refreshed)); - if (current == NULL) - { - kvm_drm_copy_error_message(out_error, out_error_size, "Current DRM output is null"); return false; } - if (!kvm_drm_find_active_output_on_fd(fd, current->device_path, &refreshed, false, out_error, out_error_size)) - { - return false; - } - - if (require_scanout && kvm_drm_get_scanout_fb_id(fd, refreshed.crtc_id, refreshed.crtc_index, NULL, NULL) == 0) - { - kvm_drm_copy_error_message(out_error, out_error_size, "No active DRM framebuffer available yet"); - return false; - } + qsort(outputs, (size_t)count, sizeof(kvm_drm_output), kvm_drm_compare_outputs); + *out_count = count; - if (drm_debug && !kvm_drm_same_output(current, &refreshed)) + if (drm_debug && logSelection) { - fprintf(stderr, "DRM: Switched output from %s/%u to %s/%u on %s\n", - current->connector_name, - current->crtc_id, - refreshed.connector_name, - refreshed.crtc_id, - refreshed.device_path); + fprintf(stderr, "DRM: Selected %d output(s) on %s\n", count, path); + for (i = 0; i < count; ++i) + { + fprintf(stderr, "DRM: output[%d] %s connector=%u crtc=%u index=%d pos=%d,%d size=%ux%u\n", + i, outputs[i].connector_name, outputs[i].connector_id, outputs[i].crtc_id, + outputs[i].crtc_index, outputs[i].x, outputs[i].y, outputs[i].width, outputs[i].height); + } } - - *current = refreshed; return true; } -static bool kvm_drm_open_device_with_output(const char *explicit_device, int *out_fd, kvm_drm_output *out, char *out_error, size_t out_error_size) +static bool kvm_drm_open_device_with_outputs(const char *explicit_device, int *out_fd, kvm_drm_output *outputs, int max_outputs, int *out_count, char *out_error, size_t out_error_size) { int i; @@ -707,7 +712,7 @@ static bool kvm_drm_open_device_with_output(const char *explicit_device, int *ou kvm_drm_copy_error_message(out_error, out_error_size, err); return false; } - if (kvm_drm_find_active_output_on_fd(fd, explicit_device, out, true, out_error, out_error_size)) + if (kvm_drm_collect_active_outputs_on_fd(fd, explicit_device, outputs, max_outputs, out_count, true, true, out_error, out_error_size)) { *out_fd = fd; return true; @@ -725,7 +730,7 @@ static bool kvm_drm_open_device_with_output(const char *explicit_device, int *ou { continue; } - if (kvm_drm_find_active_output_on_fd(fd, path, out, true, out_error, out_error_size)) + if (kvm_drm_collect_active_outputs_on_fd(fd, path, outputs, max_outputs, out_count, true, true, out_error, out_error_size)) { *out_fd = fd; return true; @@ -733,7 +738,7 @@ static bool kvm_drm_open_device_with_output(const char *explicit_device, int *ou close(fd); } - kvm_drm_copy_error_message(out_error, out_error_size, "No usable /dev/dri/card* device with an active connector/CRTC"); + kvm_drm_copy_error_message(out_error, out_error_size, "No usable /dev/dri/card* device with active connector/CRTC scanout"); return false; } @@ -1280,6 +1285,270 @@ static void kvm_drm_get_rotated_dimensions(const kvm_drm_scanout_frame *frame, u } } +static bool kvm_drm_compute_desktop_layout(const kvm_drm_output *outputs, int output_count, kvm_drm_desktop_layout *layout) +{ + int i; + int min_x = INT_MAX; + int min_y = INT_MAX; + int max_x = INT_MIN; + int max_y = INT_MIN; + + if (outputs == NULL || output_count <= 0 || layout == NULL) + { + return false; + } + + for (i = 0; i < output_count; ++i) + { + int right = outputs[i].x + (int)outputs[i].width; + int bottom = outputs[i].y + (int)outputs[i].height; + if (outputs[i].width == 0 || outputs[i].height == 0) + { + continue; + } + if (outputs[i].x < min_x) { min_x = outputs[i].x; } + if (outputs[i].y < min_y) { min_y = outputs[i].y; } + if (right > max_x) { max_x = right; } + if (bottom > max_y) { max_y = bottom; } + } + + if (min_x == INT_MAX || min_y == INT_MAX || max_x <= min_x || max_y <= min_y) + { + return false; + } + + layout->min_x = min_x; + layout->min_y = min_y; + layout->max_x = max_x; + layout->max_y = max_y; + layout->width = (uint32_t)(max_x - min_x); + layout->height = (uint32_t)(max_y - min_y); + return true; +} + +static int kvm_drm_find_output_by_name(const kvm_drm_output *outputs, int output_count, const char *name) +{ + int i; + if (outputs == NULL || name == NULL || name[0] == 0) + { + return -1; + } + for (i = 0; i < output_count; ++i) + { + if (strcmp(outputs[i].connector_name, name) == 0) + { + return i; + } + } + return -1; +} + +static int kvm_drm_apply_kwin_screen(kvm_drm_output *outputs, int output_count, const char *name, int enabled, int x, int y, uint32_t width, uint32_t height, int *matched) +{ + int index; + if (enabled != 1 || width == 0 || height == 0) + { + return 0; + } + index = kvm_drm_find_output_by_name(outputs, output_count, name); + if (index < 0) + { + return 0; + } + + outputs[index].x = x; + outputs[index].y = y; + outputs[index].width = width; + outputs[index].height = height; + if (matched != NULL) { (*matched)++; } + return 1; +} + +static bool kvm_drm_apply_kwin_layout(kvm_drm_output *outputs, int output_count, bool logSelection) +{ + FILE *pipe; + char line[256]; + kvm_drm_output tmp[KVM_DRM_MAX_OUTPUTS]; + int in_screens = 0; + int have_screen = 0; + char name[32]; + int enabled = -1; + int x = 0; + int y = 0; + uint32_t width = 0; + uint32_t height = 0; + int matched = 0; + int i; + + if (outputs == NULL || output_count <= 0 || output_count > KVM_DRM_MAX_OUTPUTS) + { + return false; + } + + memcpy_s(tmp, sizeof(tmp), outputs, sizeof(kvm_drm_output) * (size_t)output_count); + memset(name, 0, sizeof(name)); + + pipe = popen("(qdbus6 org.kde.KWin /KWin org.kde.KWin.supportInformation 2>/dev/null || qdbus org.kde.KWin /KWin org.kde.KWin.supportInformation 2>/dev/null)", "r"); + if (pipe == NULL) + { + return false; + } + + while (fgets(line, sizeof(line), pipe) != NULL) + { + if (!in_screens) + { + if (strncmp(line, "Screens", 7) == 0) + { + in_screens = 1; + } + continue; + } + if (strncmp(line, "Compositing", 11) == 0) + { + break; + } + if (strncmp(line, "Screen ", 7) == 0) + { + if (have_screen) + { + kvm_drm_apply_kwin_screen(tmp, output_count, name, enabled, x, y, width, height, &matched); + } + have_screen = 1; + name[0] = 0; + enabled = -1; + x = y = 0; + width = height = 0; + continue; + } + if (!have_screen) + { + continue; + } + if (sscanf(line, "Name: %31s", name) == 1) + { + continue; + } + if (sscanf(line, "Enabled: %d", &enabled) == 1) + { + continue; + } + if (sscanf(line, "Geometry: %d,%d,%ux%u", &x, &y, &width, &height) == 4) + { + continue; + } + } + if (have_screen) + { + kvm_drm_apply_kwin_screen(tmp, output_count, name, enabled, x, y, width, height, &matched); + } + pclose(pipe); + + if (matched != output_count) + { + return false; + } + + memcpy_s(outputs, sizeof(kvm_drm_output) * (size_t)output_count, tmp, sizeof(kvm_drm_output) * (size_t)output_count); + qsort(outputs, (size_t)output_count, sizeof(kvm_drm_output), kvm_drm_compare_outputs); + if (drm_debug && logSelection) + { + fprintf(stderr, "DRM: Using KWin logical output layout\n"); + for (i = 0; i < output_count; ++i) + { + fprintf(stderr, "DRM: kwin[%d] %s pos=%d,%d size=%ux%u\n", + i, outputs[i].connector_name, outputs[i].x, outputs[i].y, outputs[i].width, outputs[i].height); + } + } + return true; +} + +static void kvm_drm_prepare_session_environment(int sessionUid) +{ + char runtimeDir[64]; + char busAddress[96]; + if (sessionUid <= 0) + { + return; + } + snprintf(runtimeDir, sizeof(runtimeDir), "/run/user/%d", sessionUid); + snprintf(busAddress, sizeof(busAddress), "unix:path=/run/user/%d/bus", sessionUid); + if (getenv("XDG_RUNTIME_DIR") == NULL) { setenv("XDG_RUNTIME_DIR", runtimeDir, 1); } + if (getenv("DBUS_SESSION_BUS_ADDRESS") == NULL) { setenv("DBUS_SESSION_BUS_ADDRESS", busAddress, 1); } +} + +static void kvm_drm_copy_frame_to_desktop(const unsigned char *src, uint32_t src_width, uint32_t src_height, unsigned char *dst, uint32_t dst_width, uint32_t dst_height, int dst_x, int dst_y, uint32_t output_width, uint32_t output_height) +{ + uint32_t y; + if (src == NULL || dst == NULL || src_width == 0 || src_height == 0 || dst_width == 0 || dst_height == 0 || output_width == 0 || output_height == 0) + { + return; + } + + if (src_width == output_width && src_height == output_height) + { + for (y = 0; y < src_height; ++y) + { + int target_y = dst_y + (int)y; + if (target_y < 0 || target_y >= (int)dst_height) + { + continue; + } + + int copy_x = dst_x; + uint32_t src_x = 0; + uint32_t copy_width = src_width; + if (copy_x < 0) + { + src_x = (uint32_t)(-copy_x); + if (src_x >= copy_width) { continue; } + copy_width -= src_x; + copy_x = 0; + } + if (copy_x >= (int)dst_width) + { + continue; + } + if (copy_width > dst_width - (uint32_t)copy_x) + { + copy_width = dst_width - (uint32_t)copy_x; + } + + memcpy_s(dst + ((((size_t)target_y * (size_t)dst_width) + (size_t)copy_x) * 3u), + ((size_t)(dst_width - (uint32_t)copy_x)) * 3u, + src + ((((size_t)y * (size_t)src_width) + (size_t)src_x) * 3u), + ((size_t)copy_width) * 3u); + } + return; + } + + for (y = 0; y < output_height; ++y) + { + int target_y = dst_y + (int)y; + if (target_y < 0 || target_y >= (int)dst_height) + { + continue; + } + + uint32_t x; + uint32_t src_y = (uint32_t)(((uint64_t)y * (uint64_t)src_height) / (uint64_t)output_height); + for (x = 0; x < output_width; ++x) + { + int target_x = dst_x + (int)x; + if (target_x < 0 || target_x >= (int)dst_width) + { + continue; + } + uint32_t src_x = (uint32_t)(((uint64_t)x * (uint64_t)src_width) / (uint64_t)output_width); + const unsigned char *s = src + ((((size_t)src_y * (size_t)src_width) + (size_t)src_x) * 3u); + unsigned char *d = dst + ((((size_t)target_y * (size_t)dst_width) + (size_t)target_x) * 3u); + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + } + } +} + static uint64_t kvm_drm_now_ms() { struct timespec tsNow; @@ -1392,13 +1661,13 @@ void *kvm_server_mainloop_drm(void *parm) int displayListSent = 0; uint64_t lastFrameTimeMs = 0; int lastCaptureError = 0; - int64_t nFramesConverted = 0; int scanoutSuspended = 0; int forceTileReset = 0; uint64_t lastOutputRefreshMs = 0; uint64_t lastRefreshFailureLogMs = 0; char lastRefreshFailure[KVM_DRM_MAX_ERROR]; + kvm_drm_init_debug(); g_kvmBackendDRM = 1; g_enableEvents = kvm_events_evdev_init(); if (!g_enableEvents) @@ -1429,13 +1698,17 @@ void *kvm_server_mainloop_drm(void *parm) #else int fd = -1; char err[KVM_DRM_MAX_ERROR]; - kvm_drm_output output; + kvm_drm_output outputs[KVM_DRM_MAX_OUTPUTS]; + int outputCount = 0; + kvm_drm_desktop_layout layout; kvm_drm_frame_map map; kvm_drm_egl_context eglCtx; unsigned char *rgbBuffer = NULL; size_t rgbBufferSize = 0; unsigned char *rgbRotatedBuffer = NULL; size_t rgbRotatedBufferSize = 0; + unsigned char *desktopRgbBuffer = NULL; + size_t desktopRgbBufferSize = 0; char *desktopBuffer = NULL; long long desktopBufferSize = 0; uint32_t lastLoggedFbId = 0; @@ -1448,7 +1721,8 @@ void *kvm_server_mainloop_drm(void *parm) uint64_t lastLoggedModifier = UINT64_MAX; int lastLoggedPath = -1; int lastLoggedRotation = -1; - memset(&output, 0, sizeof(output)); + memset(outputs, 0, sizeof(outputs)); + memset(&layout, 0, sizeof(layout)); memset(&map, 0, sizeof(map)); memset(&eglCtx, 0, sizeof(eglCtx)); memset(lastRefreshFailure, 0, sizeof(lastRefreshFailure)); @@ -1456,9 +1730,10 @@ void *kvm_server_mainloop_drm(void *parm) map.drm_fd = -1; char *explicitDevice = getenv("MESH_KVM_DRM_DEVICE"); - if (!kvm_drm_open_device_with_output(explicitDevice, &fd, &output, err, sizeof(err))) + if (!kvm_drm_open_device_with_outputs(explicitDevice, &fd, outputs, KVM_DRM_MAX_OUTPUTS, &outputCount, err, sizeof(err)) || + !kvm_drm_compute_desktop_layout(outputs, outputCount, &layout)) { - kvm_send_error(err); + kvm_send_error(err[0] ? err : "Unable to compute DRM desktop layout"); kvm_events_evdev_shutdown(); g_enableEvents = 0; g_kvmBackendDRM = 0; @@ -1485,6 +1760,19 @@ void *kvm_server_mainloop_drm(void *parm) close(fd); return (void *)-1; } + kvm_drm_prepare_session_environment(sessionUid); + if (kvm_drm_apply_kwin_layout(outputs, outputCount, true)) + { + if (!kvm_drm_compute_desktop_layout(outputs, outputCount, &layout)) + { + kvm_send_error("Unable to compute KWin DRM desktop layout"); + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + close(fd); + return (void *)-1; + } + } while (!g_shutdown) { @@ -1553,199 +1841,180 @@ void *kvm_server_mainloop_drm(void *parm) continue; } - kvm_drm_scanout_frame frame; - size_t rgbSize = 0; - bool converted = false; - memset(&frame, 0, sizeof(frame)); - - if (!kvm_drm_get_scanout_frame(fd, output.crtc_id, output.crtc_index, &frame, err, sizeof(err))) + if (lastOutputRefreshMs == 0 || nowMs - lastOutputRefreshMs >= 1000) { - if (kvm_drm_is_transient_scanout_error(err)) + kvm_drm_output refreshed[KVM_DRM_MAX_OUTPUTS]; + kvm_drm_desktop_layout refreshedLayout; + int refreshedCount = 0; + char refreshErr[KVM_DRM_MAX_ERROR]; + memset(refreshed, 0, sizeof(refreshed)); + memset(&refreshedLayout, 0, sizeof(refreshedLayout)); + if (kvm_drm_collect_active_outputs_on_fd(fd, outputs[0].device_path, refreshed, KVM_DRM_MAX_OUTPUTS, &refreshedCount, true, false, refreshErr, sizeof(refreshErr))) { - if (!scanoutSuspended) + ignore_result(kvm_drm_apply_kwin_layout(refreshed, refreshedCount, false)); + if (kvm_drm_compute_desktop_layout(refreshed, refreshedCount, &refreshedLayout)) { - scanoutSuspended = 1; - forceTileReset = 1; - kvm_drm_destroy_map(&map); - kvm_drm_egl_destroy_context(&eglCtx); - kvm_drm_reset_logged_scanout_state(&lastLoggedFbId, &lastLoggedWidth, &lastLoggedHeight, - &lastLoggedPitch, &lastLoggedOffset, &lastLoggedFormat, &lastLoggedHandle, - &lastLoggedModifier, &lastLoggedPath); - lastLoggedRotation = -1; - if (drm_debug) + if (refreshedCount != outputCount || + memcmp(outputs, refreshed, sizeof(kvm_drm_output) * (size_t)refreshedCount) != 0 || + memcmp(&layout, &refreshedLayout, sizeof(layout)) != 0) { - fprintf(stderr, "DRM: Scanout unavailable on %s, waiting for the display to resume\n", output.connector_name); + memcpy_s(outputs, sizeof(outputs), refreshed, sizeof(kvm_drm_output) * (size_t)refreshedCount); + outputCount = refreshedCount; + layout = refreshedLayout; + forceTileReset = 1; + kvm_drm_destroy_map(&map); + kvm_drm_egl_destroy_context(&eglCtx); + kvm_drm_reset_logged_scanout_state(&lastLoggedFbId, &lastLoggedWidth, &lastLoggedHeight, + &lastLoggedPitch, &lastLoggedOffset, &lastLoggedFormat, &lastLoggedHandle, + &lastLoggedModifier, &lastLoggedPath); + lastLoggedRotation = -1; + if (drm_debug) + { + fprintf(stderr, "DRM: Refreshed desktop layout: %d output(s), origin=%d,%d size=%ux%u\n", + outputCount, layout.min_x, layout.min_y, layout.width, layout.height); + } } lastRefreshFailure[0] = 0; lastRefreshFailureLogMs = 0; } - - if (lastOutputRefreshMs == 0 || nowMs - lastOutputRefreshMs >= 250) - { - char refreshErr[KVM_DRM_MAX_ERROR]; - if (kvm_drm_refresh_output_on_fd(fd, &output, true, refreshErr, sizeof(refreshErr))) - { - lastRefreshFailure[0] = 0; - lastRefreshFailureLogMs = 0; - } - else if (drm_debug && !kvm_drm_is_expected_suspended_refresh_error(refreshErr) && - (lastRefreshFailure[0] == 0 || - strcmp(lastRefreshFailure, refreshErr) != 0 || - nowMs - lastRefreshFailureLogMs >= 5000)) - { - fprintf(stderr, "DRM: Output refresh while scanout is suspended failed: %s\n", refreshErr); - kvm_drm_copy_error_message(lastRefreshFailure, sizeof(lastRefreshFailure), refreshErr); - lastRefreshFailureLogMs = nowMs; - } - lastOutputRefreshMs = nowMs; - } - continue; } - - if (lastCaptureError == 0) + else if (drm_debug && !kvm_drm_is_expected_suspended_refresh_error(refreshErr) && + (lastRefreshFailure[0] == 0 || + strcmp(lastRefreshFailure, refreshErr) != 0 || + nowMs - lastRefreshFailureLogMs >= 5000)) { - kvm_send_error(err); - lastCaptureError = 1; + fprintf(stderr, "DRM: Output refresh failed: %s\n", refreshErr); + kvm_drm_copy_error_message(lastRefreshFailure, sizeof(lastRefreshFailure), refreshErr); + lastRefreshFailureLogMs = nowMs; } - continue; + lastOutputRefreshMs = nowMs; } - if (scanoutSuspended) + size_t desktopRgbSize = (size_t)layout.width * (size_t)layout.height * 3u; + if (desktopRgbBufferSize < desktopRgbSize) { - scanoutSuspended = 0; - lastOutputRefreshMs = 0; - lastRefreshFailure[0] = 0; - lastRefreshFailureLogMs = 0; - if (drm_debug) - { - fprintf(stderr, "DRM: Scanout resumed on %s\n", output.connector_name); - } - } - - if (frame.handle == 0 || frame.width == 0 || frame.height == 0) - { - if (lastCaptureError == 0) - { - kvm_send_error("DRM framebuffer is not readable (missing handle)"); - lastCaptureError = 1; - } - continue; - } - - if (drm_debug && - (frame.width != lastLoggedWidth || - frame.height != lastLoggedHeight || - frame.pitch != lastLoggedPitch || - frame.offset != lastLoggedOffset || - frame.format != lastLoggedFormat || - frame.modifier != lastLoggedModifier || - (int)frame.rotation != lastLoggedRotation)) - { - kvm_drm_debug_log_scanout_frame("Using scanout framebuffer", &frame); - lastLoggedWidth = frame.width; - lastLoggedHeight = frame.height; - lastLoggedPitch = frame.pitch; - lastLoggedOffset = frame.offset; - lastLoggedFormat = frame.format; - lastLoggedModifier = frame.modifier; - lastLoggedRotation = (int)frame.rotation; + unsigned char *tmp = (unsigned char *)realloc(desktopRgbBuffer, desktopRgbSize); + if (tmp == NULL) ILIBCRITICALEXIT(254); + desktopRgbBuffer = tmp; + desktopRgbBufferSize = desktopRgbSize; } + memset(desktopRgbBuffer, 0, desktopRgbSize); + int capturedOutputs = 0; + int outputIndex = 0; + for (outputIndex = 0; outputIndex < outputCount; ++outputIndex) { + kvm_drm_scanout_frame frame; + size_t rgbSize = 0; + bool converted = false; uint32_t effectiveWidth = 0; uint32_t effectiveHeight = 0; - kvm_drm_get_rotated_dimensions(&frame, &effectiveWidth, &effectiveHeight); - if (SCREEN_WIDTH != (int)effectiveWidth || SCREEN_HEIGHT != (int)effectiveHeight) + memset(&frame, 0, sizeof(frame)); + + if (!kvm_drm_get_scanout_frame(fd, outputs[outputIndex].crtc_id, outputs[outputIndex].crtc_index, &frame, err, sizeof(err))) { - int oldTileHeightCount = TILE_HEIGHT_COUNT; - SCREEN_WIDTH = (int)effectiveWidth; - SCREEN_HEIGHT = (int)effectiveHeight; - TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; - TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; - if (SCREEN_WIDTH % TILE_WIDTH) + if (kvm_drm_is_transient_scanout_error(err)) { - TILE_WIDTH_COUNT++; + scanoutSuspended = 1; + forceTileReset = 1; + continue; } - if (SCREEN_HEIGHT % TILE_HEIGHT) + if (lastCaptureError == 0) { - TILE_HEIGHT_COUNT++; + kvm_send_error(err); + lastCaptureError = 1; } - kvm_send_resolution(); - kvm_send_display(); - reset_tile_info(oldTileHeightCount); - forceTileReset = 0; + continue; } - } - if (!displayListSent) - { - kvm_send_display_list(); - displayListSent = 1; - } + if (frame.handle == 0 || frame.width == 0 || frame.height == 0) + { + if (lastCaptureError == 0) + { + kvm_send_error("DRM framebuffer is not readable (missing handle)"); + lastCaptureError = 1; + } + continue; + } - rgbSize = (size_t)frame.width * (size_t)frame.height * 3u; - if (rgbBufferSize < rgbSize) - { - unsigned char *tmp = (unsigned char *)realloc(rgbBuffer, rgbSize); - if (tmp == NULL) - ILIBCRITICALEXIT(254); - rgbBuffer = tmp; - rgbBufferSize = rgbSize; - } + if (drm_debug >= 2 && + (frame.width != lastLoggedWidth || + frame.height != lastLoggedHeight || + frame.pitch != lastLoggedPitch || + frame.offset != lastLoggedOffset || + frame.format != lastLoggedFormat || + frame.modifier != lastLoggedModifier || + (int)frame.rotation != lastLoggedRotation)) + { + fprintf(stderr, "DRM: Output %s at %d,%d\n", outputs[outputIndex].connector_name, outputs[outputIndex].x, outputs[outputIndex].y); + kvm_drm_debug_log_scanout_frame("Using scanout framebuffer", &frame); + lastLoggedWidth = frame.width; + lastLoggedHeight = frame.height; + lastLoggedPitch = frame.pitch; + lastLoggedOffset = frame.offset; + lastLoggedFormat = frame.format; + lastLoggedModifier = frame.modifier; + lastLoggedRotation = (int)frame.rotation; + } - if (frame.modifier != DRM_FORMAT_MOD_INVALID && frame.modifier != DRM_FORMAT_MOD_LINEAR) - { - if (drm_debug && lastLoggedPath != 1) + rgbSize = (size_t)frame.width * (size_t)frame.height * 3u; + if (rgbBufferSize < rgbSize) { - fprintf(stderr, "DRM: Using GPU-assisted conversion for modifier 0x%016" PRIx64 "\n", frame.modifier); - lastLoggedPath = 1; + unsigned char *tmp = (unsigned char *)realloc(rgbBuffer, rgbSize); + if (tmp == NULL) ILIBCRITICALEXIT(254); + rgbBuffer = tmp; + rgbBufferSize = rgbSize; } - converted = kvm_drm_egl_convert_to_rgb24_gpu(&eglCtx, fd, frame.width, frame.height, frame.pitch, - frame.offset, frame.format, frame.handle, frame.modifier, - rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); - kvm_drm_close_gem_handle(fd, frame.handle); - frame.handle = 0; - } - else - { - size_t required_bytes = (size_t)frame.offset + ((size_t)frame.pitch * (size_t)frame.height); - if (!kvm_drm_map_framebuffer_handle(fd, frame.handle, required_bytes, &map, err, sizeof(err))) + if (frame.modifier != DRM_FORMAT_MOD_INVALID && frame.modifier != DRM_FORMAT_MOD_LINEAR) { + if (drm_debug >= 2 && lastLoggedPath != 1) + { + fprintf(stderr, "DRM: Using GPU-assisted conversion for modifier 0x%016" PRIx64 "\n", frame.modifier); + lastLoggedPath = 1; + } + + converted = kvm_drm_egl_convert_to_rgb24_gpu(&eglCtx, fd, frame.width, frame.height, frame.pitch, + frame.offset, frame.format, frame.handle, frame.modifier, + rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); kvm_drm_close_gem_handle(fd, frame.handle); - converted = false; + frame.handle = 0; } else { - if (drm_debug && lastLoggedPath != 0) + size_t required_bytes = (size_t)frame.offset + ((size_t)frame.pitch * (size_t)frame.height); + if (!kvm_drm_map_framebuffer_handle(fd, frame.handle, required_bytes, &map, err, sizeof(err))) { - char format[32]; - kvm_drm_format_fourcc(format, sizeof(format), frame.format); - fprintf(stderr, "DRM: Using CPU readback conversion for format %s\n", format); - lastLoggedPath = 0; + kvm_drm_close_gem_handle(fd, frame.handle); + converted = false; } + else + { + if (drm_debug >= 2 && lastLoggedPath != 0) + { + char format[32]; + kvm_drm_format_fourcc(format, sizeof(format), frame.format); + fprintf(stderr, "DRM: Using CPU readback conversion for format %s\n", format); + lastLoggedPath = 0; + } - const uint8_t *src = map.addr + frame.offset; - converted = kvm_drm_convert_to_rgb24(&frame, src, rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); + const uint8_t *src = map.addr + frame.offset; + converted = kvm_drm_convert_to_rgb24(&frame, src, rgbBuffer, rgbBufferSize, &rgbSize, err, sizeof(err)); + } } - } - if (!converted) - { - if (lastCaptureError == 0) + if (!converted) { - kvm_send_error(err); - lastCaptureError = 1; + if (lastCaptureError == 0) + { + kvm_send_error(err); + lastCaptureError = 1; + } + continue; } - continue; - } - lastCaptureError = 0; - nFramesConverted++; - { const unsigned char *rgbFrame = rgbBuffer; - size_t rgbFrameSize = rgbSize; - + kvm_drm_get_rotated_dimensions(&frame, &effectiveWidth, &effectiveHeight); if (frame.rotation != KVM_DRM_ROTATION_0) { if (rgbRotatedBufferSize < rgbSize) @@ -1759,21 +2028,64 @@ void *kvm_server_mainloop_drm(void *parm) rgbFrame = rgbRotatedBuffer; } - if (g_tileInfo == NULL) + kvm_drm_copy_frame_to_desktop(rgbFrame, effectiveWidth, effectiveHeight, desktopRgbBuffer, layout.width, layout.height, + outputs[outputIndex].x - layout.min_x, outputs[outputIndex].y - layout.min_y, + outputs[outputIndex].width, outputs[outputIndex].height); + capturedOutputs++; + } + + if (capturedOutputs <= 0) + { + if (scanoutSuspended && drm_debug) { - reset_tile_info(0); - forceTileReset = 0; + fprintf(stderr, "DRM: Scanout unavailable on all outputs, waiting for display resume\n"); } - else if (forceTileReset) + continue; + } + scanoutSuspended = 0; + lastCaptureError = 0; + + if (SCREEN_WIDTH != (int)layout.width || SCREEN_HEIGHT != (int)layout.height) + { + int oldTileHeightCount = TILE_HEIGHT_COUNT; + SCREEN_WIDTH = (int)layout.width; + SCREEN_HEIGHT = (int)layout.height; + TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; + TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; + if (SCREEN_WIDTH % TILE_WIDTH) { - reset_tile_info(TILE_HEIGHT_COUNT); - forceTileReset = 0; + TILE_WIDTH_COUNT++; } - - if (kvm_drm_send_dirty_tiles(rgbFrame, rgbFrameSize, &desktopBuffer, &desktopBufferSize) != 0) + if (SCREEN_HEIGHT % TILE_HEIGHT) { - g_shutdown = 1; + TILE_HEIGHT_COUNT++; } + kvm_send_resolution(); + kvm_send_display(); + reset_tile_info(oldTileHeightCount); + forceTileReset = 0; + } + + if (!displayListSent) + { + kvm_send_display_list(); + displayListSent = 1; + } + + if (g_tileInfo == NULL) + { + reset_tile_info(0); + forceTileReset = 0; + } + else if (forceTileReset) + { + reset_tile_info(TILE_HEIGHT_COUNT); + forceTileReset = 0; + } + + if (kvm_drm_send_dirty_tiles(desktopRgbBuffer, desktopRgbSize, &desktopBuffer, &desktopBufferSize) != 0) + { + g_shutdown = 1; } } @@ -1800,6 +2112,11 @@ void *kvm_server_mainloop_drm(void *parm) free(rgbRotatedBuffer); rgbRotatedBuffer = NULL; } + if (desktopRgbBuffer != NULL) + { + free(desktopRgbBuffer); + desktopRgbBuffer = NULL; + } kvm_drm_destroy_map(&map); kvm_drm_egl_destroy_context(&eglCtx); if (fd >= 0) diff --git a/meshcore/KVM/Linux/linux_kvm_drm_egl.c b/meshcore/KVM/Linux/linux_kvm_drm_egl.c index b63122e0d..d404dd209 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm_egl.c +++ b/meshcore/KVM/Linux/linux_kvm_drm_egl.c @@ -191,11 +191,14 @@ static bool kvm_drm_egl_init_gpu_readback(kvm_drm_egl_context *g, char *out_erro kvm_drm_egl_format_egl_error(err, sizeof(err), "eglInitialize failed", eglGetError()); return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, err); } - fprintf(stderr, "DRM EGL: Initialized EGL %d.%d vendor=%s version=%s\n", - (int)major, - (int)minor, - eglQueryString(g->dpy, EGL_VENDOR) != NULL ? eglQueryString(g->dpy, EGL_VENDOR) : "unknown", - eglQueryString(g->dpy, EGL_VERSION) != NULL ? eglQueryString(g->dpy, EGL_VERSION) : "unknown"); + if (getenv("MESH_KVM_DRM_DEBUG") != NULL && atoi(getenv("MESH_KVM_DRM_DEBUG")) > 0) + { + fprintf(stderr, "DRM EGL: Initialized EGL %d.%d vendor=%s version=%s\n", + (int)major, + (int)minor, + eglQueryString(g->dpy, EGL_VENDOR) != NULL ? eglQueryString(g->dpy, EGL_VENDOR) : "unknown", + eglQueryString(g->dpy, EGL_VERSION) != NULL ? eglQueryString(g->dpy, EGL_VERSION) : "unknown"); + } if (!eglBindAPI(EGL_OPENGL_ES_API)) { From ee8aa56ea2873c1ff97918380bf43568b256ea3c Mon Sep 17 00:00:00 2001 From: Patrick O'Connell Date: Tue, 26 May 2026 08:29:16 +1000 Subject: [PATCH 11/14] Require Wayland client support for Linux KVM builds Make Wayland client support mandatory for Linux KVM builds so release and CI builds cannot silently omit the generic xdg-output monitor layout path. The DRM backend now includes wayland-client directly, and the makefile fails early when the wayland-client development package is unavailable. This keeps Wayland monitor positioning and scaling behavior consistent between build servers and deployed agents. --- makefile | 6 + meshcore/KVM/Linux/linux_kvm_drm.c | 388 ++++++++++++++++++++++++++++- microscript/ILibDuktape_Commit.h | 8 +- 3 files changed, 394 insertions(+), 8 deletions(-) diff --git a/makefile b/makefile index 62de1d145..0b3cbafcc 100644 --- a/makefile +++ b/makefile @@ -546,9 +546,15 @@ MACOSKVMSOURCES = meshcore/KVM/MacOS/mac_kvm.c meshcore/KVM/MacOS/mac_events.c m CFLAGS += -D_LINKVM DRMCFLAGS = $(shell pkg-config --cflags libdrm egl glesv2 2>/dev/null) DRMLIBS = $(shell pkg-config --libs libdrm egl glesv2 2>/dev/null) + WAYLANDCLIENT = $(shell pkg-config --exists wayland-client 2>/dev/null && echo 1) ifneq ($(strip $(DRMCFLAGS)),) CFLAGS += $(DRMCFLAGS) endif + ifneq ($(WAYLANDCLIENT),1) + $(error Linux KVM builds require the wayland-client development package) + endif + CFLAGS += -DHAVE_WAYLAND_CLIENT $(shell pkg-config --cflags wayland-client) + DRMLIBS += $(shell pkg-config --libs wayland-client) ifneq ($(JPEGVER),) ifeq ($(LEGACY_LD),1) LINUXFLAGS = lib-jpeg-turbo/linux/$(ARCHNAME)/$(JPEGVER)/libturbojpeg.a diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index da0c475a3..2557cdbdc 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -39,6 +39,7 @@ limitations under the License. #if defined(__linux__) #include +#include #include #include #include @@ -50,6 +51,8 @@ limitations under the License. #include #include +#include + #ifndef O_CLOEXEC #define KVM_DRM_O_CLOEXEC 0 #else @@ -1364,6 +1367,364 @@ static int kvm_drm_apply_kwin_screen(kvm_drm_output *outputs, int output_count, return 1; } +struct zxdg_output_manager_v1; +struct zxdg_output_v1; + +struct zxdg_output_v1_listener +{ + void (*logical_position)(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y); + void (*logical_size)(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t width, int32_t height); + void (*done)(void *data, struct zxdg_output_v1 *zxdg_output_v1); + void (*name)(void *data, struct zxdg_output_v1 *zxdg_output_v1, const char *name); + void (*description)(void *data, struct zxdg_output_v1 *zxdg_output_v1, const char *description); +}; + +extern const struct wl_interface wl_output_interface; +static const struct wl_interface zxdg_output_v1_interface; + +static const struct wl_interface *kvm_xdg_output_types[] = +{ + NULL, + NULL, + &zxdg_output_v1_interface, + &wl_output_interface, +}; + +static const struct wl_message zxdg_output_manager_v1_requests[] = +{ + { "destroy", "", kvm_xdg_output_types + 0 }, + { "get_xdg_output", "no", kvm_xdg_output_types + 2 }, +}; + +static const struct wl_interface zxdg_output_manager_v1_interface = +{ + "zxdg_output_manager_v1", 3, + 2, zxdg_output_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message zxdg_output_v1_requests[] = +{ + { "destroy", "", kvm_xdg_output_types + 0 }, +}; + +static const struct wl_message zxdg_output_v1_events[] = +{ + { "logical_position", "ii", kvm_xdg_output_types + 0 }, + { "logical_size", "ii", kvm_xdg_output_types + 0 }, + { "done", "", kvm_xdg_output_types + 0 }, + { "name", "2s", kvm_xdg_output_types + 0 }, + { "description", "2s", kvm_xdg_output_types + 0 }, +}; + +static const struct wl_interface zxdg_output_v1_interface = +{ + "zxdg_output_v1", 3, + 1, zxdg_output_v1_requests, + 5, zxdg_output_v1_events, +}; + +static struct zxdg_output_v1 *zxdg_output_manager_v1_get_xdg_output(struct zxdg_output_manager_v1 *manager, struct wl_output *output) +{ + struct wl_proxy *id = wl_proxy_marshal_flags((struct wl_proxy *)manager, 1, &zxdg_output_v1_interface, wl_proxy_get_version((struct wl_proxy *)manager), 0, NULL, output); + return (struct zxdg_output_v1 *)id; +} + +static void zxdg_output_manager_v1_destroy(struct zxdg_output_manager_v1 *manager) +{ + wl_proxy_marshal_flags((struct wl_proxy *)manager, 0, NULL, wl_proxy_get_version((struct wl_proxy *)manager), WL_MARSHAL_FLAG_DESTROY); +} + +static int zxdg_output_v1_add_listener(struct zxdg_output_v1 *output, const struct zxdg_output_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *)output, (void (**)(void))listener, data); +} + +static void zxdg_output_v1_destroy(struct zxdg_output_v1 *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *)output, 0, NULL, wl_proxy_get_version((struct wl_proxy *)output), WL_MARSHAL_FLAG_DESTROY); +} + +typedef struct kvm_drm_wayland_output +{ + struct wl_output *wl_output; + struct zxdg_output_v1 *xdg_output; + uint32_t global_name; + uint32_t version; + char name[64]; + int have_name; + int have_position; + int have_size; + int x; + int y; + uint32_t width; + uint32_t height; +} kvm_drm_wayland_output; + +typedef struct kvm_drm_wayland_layout_context +{ + struct wl_display *display; + struct wl_registry *registry; + struct zxdg_output_manager_v1 *xdg_output_manager; + kvm_drm_wayland_output outputs[KVM_DRM_MAX_OUTPUTS]; + int output_count; +} kvm_drm_wayland_layout_context; + +static void kvm_drm_wl_output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) +{ + (void)data; (void)wl_output; (void)x; (void)y; (void)physical_width; (void)physical_height; (void)subpixel; (void)make; (void)model; (void)transform; +} + +static void kvm_drm_wl_output_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) +{ + (void)data; (void)wl_output; (void)flags; (void)width; (void)height; (void)refresh; +} + +static void kvm_drm_wl_output_done(void *data, struct wl_output *wl_output) +{ + (void)data; (void)wl_output; +} + +static void kvm_drm_wl_output_scale(void *data, struct wl_output *wl_output, int32_t factor) +{ + (void)data; (void)wl_output; (void)factor; +} + +static void kvm_drm_wl_output_name(void *data, struct wl_output *wl_output, const char *name) +{ + kvm_drm_wayland_output *output = (kvm_drm_wayland_output *)data; + (void)wl_output; + if (output != NULL && name != NULL && output->have_name == 0) + { + snprintf(output->name, sizeof(output->name), "%s", name); + output->have_name = 1; + } +} + +static void kvm_drm_wl_output_description(void *data, struct wl_output *wl_output, const char *description) +{ + (void)data; (void)wl_output; (void)description; +} + +static const struct wl_output_listener kvm_drm_wl_output_listener = +{ + kvm_drm_wl_output_geometry, + kvm_drm_wl_output_mode, + kvm_drm_wl_output_done, + kvm_drm_wl_output_scale, + kvm_drm_wl_output_name, + kvm_drm_wl_output_description, +}; + +static void kvm_drm_xdg_output_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) +{ + kvm_drm_wayland_output *output = (kvm_drm_wayland_output *)data; + (void)xdg_output; + if (output == NULL) { return; } + output->x = x; + output->y = y; + output->have_position = 1; +} + +static void kvm_drm_xdg_output_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) +{ + kvm_drm_wayland_output *output = (kvm_drm_wayland_output *)data; + (void)xdg_output; + if (output == NULL || width <= 0 || height <= 0) { return; } + output->width = (uint32_t)width; + output->height = (uint32_t)height; + output->have_size = 1; +} + +static void kvm_drm_xdg_output_done(void *data, struct zxdg_output_v1 *xdg_output) +{ + (void)data; (void)xdg_output; +} + +static void kvm_drm_xdg_output_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) +{ + kvm_drm_wayland_output *output = (kvm_drm_wayland_output *)data; + (void)xdg_output; + if (output != NULL && name != NULL) + { + snprintf(output->name, sizeof(output->name), "%s", name); + output->have_name = 1; + } +} + +static void kvm_drm_xdg_output_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) +{ + (void)data; (void)xdg_output; (void)description; +} + +static const struct zxdg_output_v1_listener kvm_drm_xdg_output_listener = +{ + kvm_drm_xdg_output_position, + kvm_drm_xdg_output_size, + kvm_drm_xdg_output_done, + kvm_drm_xdg_output_name, + kvm_drm_xdg_output_description, +}; + +static void kvm_drm_registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) +{ + kvm_drm_wayland_layout_context *ctx = (kvm_drm_wayland_layout_context *)data; + if (ctx == NULL || interface == NULL) { return; } + + if (strcmp(interface, "wl_output") == 0) + { + kvm_drm_wayland_output *output; + uint32_t bind_version; + if (ctx->output_count >= KVM_DRM_MAX_OUTPUTS) { return; } + output = &ctx->outputs[ctx->output_count++]; + memset(output, 0, sizeof(*output)); + output->global_name = name; + output->version = version; + bind_version = version >= 4 ? 4 : version; + if (bind_version < 2) { bind_version = 2; } + output->wl_output = (struct wl_output *)wl_registry_bind(registry, name, &wl_output_interface, bind_version); + if (output->wl_output != NULL) + { + wl_output_add_listener(output->wl_output, &kvm_drm_wl_output_listener, output); + } + return; + } + + if (strcmp(interface, "zxdg_output_manager_v1") == 0) + { + uint32_t bind_version = version >= 3 ? 3 : version; + ctx->xdg_output_manager = (struct zxdg_output_manager_v1 *)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, bind_version); + } +} + +static void kvm_drm_registry_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + (void)data; (void)registry; (void)name; +} + +static const struct wl_registry_listener kvm_drm_registry_listener = +{ + kvm_drm_registry_global, + kvm_drm_registry_global_remove, +}; + +static void kvm_drm_wayland_layout_context_cleanup(kvm_drm_wayland_layout_context *ctx) +{ + int i; + if (ctx == NULL) { return; } + for (i = 0; i < ctx->output_count; ++i) + { + if (ctx->outputs[i].xdg_output != NULL) + { + zxdg_output_v1_destroy(ctx->outputs[i].xdg_output); + ctx->outputs[i].xdg_output = NULL; + } + if (ctx->outputs[i].wl_output != NULL) + { + wl_proxy_destroy((struct wl_proxy *)ctx->outputs[i].wl_output); + ctx->outputs[i].wl_output = NULL; + } + } + if (ctx->xdg_output_manager != NULL) + { + zxdg_output_manager_v1_destroy(ctx->xdg_output_manager); + ctx->xdg_output_manager = NULL; + } + if (ctx->registry != NULL) + { + wl_registry_destroy(ctx->registry); + ctx->registry = NULL; + } + if (ctx->display != NULL) + { + wl_display_disconnect(ctx->display); + ctx->display = NULL; + } +} + +static bool kvm_drm_apply_xdg_output_layout(kvm_drm_output *outputs, int output_count, bool logSelection) +{ + kvm_drm_wayland_layout_context ctx; + kvm_drm_output tmp[KVM_DRM_MAX_OUTPUTS]; + int matched = 0; + int i; + + if (outputs == NULL || output_count <= 0 || output_count > KVM_DRM_MAX_OUTPUTS) + { + return false; + } + + memset(&ctx, 0, sizeof(ctx)); + memcpy_s(tmp, sizeof(tmp), outputs, sizeof(kvm_drm_output) * (size_t)output_count); + + ctx.display = wl_display_connect(NULL); + if (ctx.display == NULL) + { + return false; + } + ctx.registry = wl_display_get_registry(ctx.display); + if (ctx.registry == NULL) + { + kvm_drm_wayland_layout_context_cleanup(&ctx); + return false; + } + wl_registry_add_listener(ctx.registry, &kvm_drm_registry_listener, &ctx); + if (wl_display_roundtrip(ctx.display) < 0 || ctx.xdg_output_manager == NULL || ctx.output_count <= 0) + { + kvm_drm_wayland_layout_context_cleanup(&ctx); + return false; + } + + for (i = 0; i < ctx.output_count; ++i) + { + if (ctx.outputs[i].wl_output == NULL) { continue; } + ctx.outputs[i].xdg_output = zxdg_output_manager_v1_get_xdg_output(ctx.xdg_output_manager, ctx.outputs[i].wl_output); + if (ctx.outputs[i].xdg_output != NULL) + { + zxdg_output_v1_add_listener(ctx.outputs[i].xdg_output, &kvm_drm_xdg_output_listener, &ctx.outputs[i]); + } + } + + for (i = 0; i < 3; ++i) + { + if (wl_display_roundtrip(ctx.display) < 0) + { + kvm_drm_wayland_layout_context_cleanup(&ctx); + return false; + } + } + + for (i = 0; i < ctx.output_count; ++i) + { + if (ctx.outputs[i].have_name == 0 || ctx.outputs[i].have_position == 0 || ctx.outputs[i].have_size == 0) + { + continue; + } + kvm_drm_apply_kwin_screen(tmp, output_count, ctx.outputs[i].name, 1, ctx.outputs[i].x, ctx.outputs[i].y, ctx.outputs[i].width, ctx.outputs[i].height, &matched); + } + + if (matched != output_count) + { + kvm_drm_wayland_layout_context_cleanup(&ctx); + return false; + } + + memcpy_s(outputs, sizeof(kvm_drm_output) * (size_t)output_count, tmp, sizeof(kvm_drm_output) * (size_t)output_count); + qsort(outputs, (size_t)output_count, sizeof(kvm_drm_output), kvm_drm_compare_outputs); + if (drm_debug && logSelection) + { + fprintf(stderr, "DRM: Using Wayland xdg-output logical layout\n"); + for (i = 0; i < output_count; ++i) + { + fprintf(stderr, "DRM: xdg-output[%d] %s pos=%d,%d size=%ux%u\n", + i, outputs[i].connector_name, outputs[i].x, outputs[i].y, outputs[i].width, outputs[i].height); + } + } + + kvm_drm_wayland_layout_context_cleanup(&ctx); + return true; +} + static bool kvm_drm_apply_kwin_layout(kvm_drm_output *outputs, int output_count, bool logSelection) { FILE *pipe; @@ -1467,6 +1828,8 @@ static void kvm_drm_prepare_session_environment(int sessionUid) { char runtimeDir[64]; char busAddress[96]; + DIR *dir = NULL; + struct dirent *ent = NULL; if (sessionUid <= 0) { return; @@ -1475,6 +1838,22 @@ static void kvm_drm_prepare_session_environment(int sessionUid) snprintf(busAddress, sizeof(busAddress), "unix:path=/run/user/%d/bus", sessionUid); if (getenv("XDG_RUNTIME_DIR") == NULL) { setenv("XDG_RUNTIME_DIR", runtimeDir, 1); } if (getenv("DBUS_SESSION_BUS_ADDRESS") == NULL) { setenv("DBUS_SESSION_BUS_ADDRESS", busAddress, 1); } + if (getenv("WAYLAND_DISPLAY") == NULL) + { + dir = opendir(runtimeDir); + if (dir != NULL) + { + while ((ent = readdir(dir)) != NULL) + { + if (strncmp(ent->d_name, "wayland-", 8) == 0 && strstr(ent->d_name, ".lock") == NULL) + { + setenv("WAYLAND_DISPLAY", ent->d_name, 1); + break; + } + } + closedir(dir); + } + } } static void kvm_drm_copy_frame_to_desktop(const unsigned char *src, uint32_t src_width, uint32_t src_height, unsigned char *dst, uint32_t dst_width, uint32_t dst_height, int dst_x, int dst_y, uint32_t output_width, uint32_t output_height) @@ -1761,11 +2140,11 @@ void *kvm_server_mainloop_drm(void *parm) return (void *)-1; } kvm_drm_prepare_session_environment(sessionUid); - if (kvm_drm_apply_kwin_layout(outputs, outputCount, true)) + if (kvm_drm_apply_xdg_output_layout(outputs, outputCount, true) || kvm_drm_apply_kwin_layout(outputs, outputCount, true)) { if (!kvm_drm_compute_desktop_layout(outputs, outputCount, &layout)) { - kvm_send_error("Unable to compute KWin DRM desktop layout"); + kvm_send_error("Unable to compute Wayland DRM desktop layout"); kvm_events_evdev_shutdown(); g_enableEvents = 0; g_kvmBackendDRM = 0; @@ -1851,7 +2230,10 @@ void *kvm_server_mainloop_drm(void *parm) memset(&refreshedLayout, 0, sizeof(refreshedLayout)); if (kvm_drm_collect_active_outputs_on_fd(fd, outputs[0].device_path, refreshed, KVM_DRM_MAX_OUTPUTS, &refreshedCount, true, false, refreshErr, sizeof(refreshErr))) { - ignore_result(kvm_drm_apply_kwin_layout(refreshed, refreshedCount, false)); + if (!kvm_drm_apply_xdg_output_layout(refreshed, refreshedCount, false)) + { + ignore_result(kvm_drm_apply_kwin_layout(refreshed, refreshedCount, false)); + } if (kvm_drm_compute_desktop_layout(refreshed, refreshedCount, &refreshedLayout)) { if (refreshedCount != outputCount || diff --git a/microscript/ILibDuktape_Commit.h b/microscript/ILibDuktape_Commit.h index 55b81d8ac..101bfee62 100644 --- a/microscript/ILibDuktape_Commit.h +++ b/microscript/ILibDuktape_Commit.h @@ -1,5 +1,3 @@ -// This file is auto-generated, any edits may be overwritten -#define SOURCE_COMMIT_DATE "2026-Feb-15 16:43:44-0800" -#define SOURCE_COMMIT_DATE "2026-Feb-15 16:43:44-0800" -#define SOURCE_COMMIT_HASH "62b206e0b485b296e8a73a6547cef02bbf5a2d62" -#define SOURCE_COMMIT_HASH "62b206e0b485b296e8a73a6547cef02bbf5a2d62" +// This file is auto-generated, any edits may be overwritten +#define SOURCE_COMMIT_DATE "2026-May-25 16:08:01+1000" +#define SOURCE_COMMIT_HASH "3602aed5f673fef272c53777200e43d825a812d4" From 383138a93623caf6152f437eeec590d456adc9c4 Mon Sep 17 00:00:00 2001 From: Patrick O'Connell Date: Thu, 4 Jun 2026 08:38:25 +1000 Subject: [PATCH 12/14] Add DRM monitor selection support Populate the existing monitor list from DRM/Wayland logical outputs and advertise the normal multi-monitor display list in DRM mode. Allow MNG_KVM_SET_DISPLAY to select all monitors or an individual monitor, crop DRM capture to the selected output, and keep evdev absolute input mapped against the full virtual desktop so pointer control remains correct across non-origin displays. --- .gitignore | 3 + meshcore/KVM/Linux/linux_events_evdev.c | 19 +++++- meshcore/KVM/Linux/linux_kvm.c | 84 ++++++++++++++----------- meshcore/KVM/Linux/linux_kvm.h | 14 +++++ meshcore/KVM/Linux/linux_kvm_drm.c | 80 ++++++++++++++++++----- 5 files changed, 147 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index 27118e83f..ff20f1056 100644 --- a/.gitignore +++ b/.gitignore @@ -330,6 +330,9 @@ ASALocalRun/ # Klocwork .klocwork/ +# Generated module export workspace +modules_expanded/ + *.o ToolChains/ .DS_Store \ No newline at end of file diff --git a/meshcore/KVM/Linux/linux_events_evdev.c b/meshcore/KVM/Linux/linux_events_evdev.c index 0d0ba73f8..c37bae936 100644 --- a/meshcore/KVM/Linux/linux_events_evdev.c +++ b/meshcore/KVM/Linux/linux_events_evdev.c @@ -66,6 +66,11 @@ static kvm_evdev_state g_kvm_evdev_state = {0}; extern int SCREEN_WIDTH; extern int SCREEN_HEIGHT; +extern int CAPTURE_X; +extern int CAPTURE_Y; +extern int VSCREEN_WIDTH; +extern int VSCREEN_HEIGHT; +extern int g_kvmBackendDRM; static const unsigned int g_kvm_evdev_alpha_keycodes[26] = { KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, @@ -735,6 +740,8 @@ void kvm_events_evdev_mouse_action(double absX, double absY, int button, short w { int x = (int)absX; int y = (int)absY; + int maxX = SCREEN_WIDTH; + int maxY = SCREEN_HEIGHT; unsigned int mouseCode = 0; int mouseValue = 0; @@ -747,11 +754,19 @@ void kvm_events_evdev_mouse_action(double absX, double absY, int button, short w return; } - if (kvm_events_evdev_write(EV_ABS, ABS_X, kvm_events_evdev_scale_axis(x, SCREEN_WIDTH)) != 0) + if (g_kvmBackendDRM != 0 && VSCREEN_WIDTH > 0 && VSCREEN_HEIGHT > 0) + { + x += CAPTURE_X; + y += CAPTURE_Y; + maxX = VSCREEN_WIDTH; + maxY = VSCREEN_HEIGHT; + } + + if (kvm_events_evdev_write(EV_ABS, ABS_X, kvm_events_evdev_scale_axis(x, maxX)) != 0) { return; } - if (kvm_events_evdev_write(EV_ABS, ABS_Y, kvm_events_evdev_scale_axis(y, SCREEN_HEIGHT)) != 0) + if (kvm_events_evdev_write(EV_ABS, ABS_Y, kvm_events_evdev_scale_axis(y, maxY)) != 0) { return; } diff --git a/meshcore/KVM/Linux/linux_kvm.c b/meshcore/KVM/Linux/linux_kvm.c index 1d6873418..07b1e352e 100644 --- a/meshcore/KVM/Linux/linux_kvm.c +++ b/meshcore/KVM/Linux/linux_kvm.c @@ -135,8 +135,6 @@ int slave2master[2]; char CURRENT_XDISPLAY[256]; int CURRENT_DISPLAY_ID = -1; -typedef struct kvm_monitor_info { int id, x, y, width, height; } kvm_monitor_info; -#define KVM_MAX_MONITORS 16 kvm_monitor_info g_monitors[KVM_MAX_MONITORS]; int g_monitor_count = 0; int SCREEN_SEL = 0; // 0 = all monitors, 1..N = specific physical monitor @@ -446,8 +444,7 @@ void kvm_send_resolution() void kvm_send_display() { char buffer[6]; - unsigned short sel = (g_kvmBackendDRM != 0) ? 0 : - ((g_monitor_count > 0) ? ((SCREEN_SEL == 0) ? (unsigned short)65535 : (unsigned short)SCREEN_SEL) : (unsigned short)CURRENT_DISPLAY_ID); + unsigned short sel = (g_monitor_count > 0) ? ((SCREEN_SEL == 0) ? (unsigned short)65535 : (unsigned short)SCREEN_SEL) : (unsigned short)CURRENT_DISPLAY_ID; ((unsigned short*)buffer)[0] = (unsigned short)htons((unsigned short)MNG_KVM_SET_DISPLAY); // Write the type ((unsigned short*)buffer)[1] = (unsigned short)htons((unsigned short)6); // Write the size ((unsigned short*)buffer)[2] = (unsigned short)htons(sel); // Display selection @@ -471,6 +468,53 @@ int lockfileCheckFn(const struct dirent *ent) { return 0; } +void kvm_apply_monitor_selection() +{ + if (SCREEN_SEL > 0 && (SCREEN_SEL - 1) < g_monitor_count) + { + int idx = SCREEN_SEL - 1; + CAPTURE_X = g_monitors[idx].x; + CAPTURE_Y = g_monitors[idx].y; + SCREEN_WIDTH = g_monitors[idx].width; + SCREEN_HEIGHT = g_monitors[idx].height; + } + else + { + SCREEN_SEL = 0; + SCREEN_SEL_TARGET = 0; + CAPTURE_X = 0; + CAPTURE_Y = 0; + SCREEN_WIDTH = VSCREEN_WIDTH; + SCREEN_HEIGHT = VSCREEN_HEIGHT; + } +} + +void kvm_update_monitor_layout(kvm_monitor_info *monitors, int monitorCount, int virtualWidth, int virtualHeight) +{ + int i; + if (monitorCount < 0) { monitorCount = 0; } + if (monitorCount > KVM_MAX_MONITORS) { monitorCount = KVM_MAX_MONITORS; } + + g_monitor_count = 0; + for (i = 0; i < monitorCount; ++i) + { + if (monitors[i].width <= 0 || monitors[i].height <= 0) { continue; } + g_monitors[g_monitor_count] = monitors[i]; + g_monitors[g_monitor_count].id = g_monitor_count + 1; + g_monitor_count++; + } + + VSCREEN_WIDTH = virtualWidth; + VSCREEN_HEIGHT = virtualHeight; + if (g_monitor_count == 0 && (VSCREEN_WIDTH <= 0 || VSCREEN_HEIGHT <= 0)) + { + VSCREEN_WIDTH = SCREEN_WIDTH; + VSCREEN_HEIGHT = SCREEN_HEIGHT; + } + + kvm_apply_monitor_selection(); +} + // Enumerate physical monitors via XRandR into g_monitors[] and update capture region. // Falls back gracefully if XRandR is unavailable. static void kvm_enumerate_monitors() @@ -503,23 +547,7 @@ static void kvm_enumerate_monitors() xrandr_exports->XRRFreeScreenResources(res); } } - // Update the capture region based on current SCREEN_SEL - if (SCREEN_SEL > 0 && (SCREEN_SEL - 1) < g_monitor_count) - { - int idx = SCREEN_SEL - 1; - CAPTURE_X = g_monitors[idx].x; - CAPTURE_Y = g_monitors[idx].y; - SCREEN_WIDTH = g_monitors[idx].width; - SCREEN_HEIGHT = g_monitors[idx].height; - } - else - { - SCREEN_SEL = 0; - CAPTURE_X = 0; - CAPTURE_Y = 0; - SCREEN_WIDTH = VSCREEN_WIDTH; - SCREEN_HEIGHT = VSCREEN_HEIGHT; - } + kvm_apply_monitor_selection(); } // Send MNG_KVM_DISPLAY_INFO: per-monitor geometry (ID, X, Y, W, H) matching Windows format. @@ -528,7 +556,6 @@ void kvm_send_display_info() int i; int totalSize; char *buffer; - if (g_kvmBackendDRM != 0) { return; } if (g_monitor_count == 0) { return; } totalSize = 4 + (g_monitor_count * 10); // 4-byte header + 10 bytes (5 shorts) per monitor buffer = (char*)ILibMemory_SmartAllocate(totalSize); @@ -550,13 +577,6 @@ void kvm_send_display_info() void getAvailableDisplays(unsigned short **array, int *len) { int i; - if (g_kvmBackendDRM != 0) - { - *len = 1; - if ((*array = (unsigned short*)malloc(sizeof(unsigned short))) == NULL) ILIBCRITICALEXIT(254); - (*array)[0] = 0; - return; - } if (g_monitor_count > 0) { // XRandR mode: expose 65535 ("all") + 1-based physical monitor IDs @@ -946,12 +966,6 @@ int kvm_server_inputdata(char* block, int blocklen) } case MNG_KVM_SET_DISPLAY: { - if (g_kvmBackendDRM != 0) - { - CURRENT_DISPLAY_ID = 0; - change_display = 0; - break; - } unsigned short newval = ntohs(((unsigned short*)(block))[2]); if (g_monitor_count > 0) { diff --git a/meshcore/KVM/Linux/linux_kvm.h b/meshcore/KVM/Linux/linux_kvm.h index b7c8af2cf..4dd9df5fb 100644 --- a/meshcore/KVM/Linux/linux_kvm.h +++ b/meshcore/KVM/Linux/linux_kvm.h @@ -40,11 +40,25 @@ limitations under the License. typedef ILibTransport_DoneState(*ILibKVM_WriteHandler)(char *buffer, int bufferLen, void *reserved); +#define KVM_MAX_MONITORS 16 +typedef struct kvm_monitor_info { int id, x, y, width, height; } kvm_monitor_info; + +extern kvm_monitor_info g_monitors[KVM_MAX_MONITORS]; +extern int g_monitor_count; +extern int SCREEN_SEL; +extern int SCREEN_SEL_TARGET; +extern int CAPTURE_X; +extern int CAPTURE_Y; +extern int VSCREEN_WIDTH; +extern int VSCREEN_HEIGHT; + void kvm_set_x11_locations(char *libx11, char *libx11tst, char *libx11ext, char *libxfixes, char *libx11kb); int kvm_relay_feeddata(char* buf, int len); void kvm_pause(int pause); void* kvm_relay_setup(void *processPipeMgr, ILibKVM_WriteHandler writeHandler, void *reserved, int uid, char *authToken, char *dispid); void kvm_relay_reset(); void kvm_cleanup(); +void kvm_update_monitor_layout(kvm_monitor_info *monitors, int monitorCount, int virtualWidth, int virtualHeight); +void kvm_apply_monitor_selection(); #endif /* LINUX_KVM_H_ */ diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index 2557cdbdc..e743dc4b8 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -80,6 +80,7 @@ extern struct tileInfo_t **g_tileInfo; extern int g_remotepause; extern int g_pause; extern int g_shutdown; +extern int change_display; extern int master2slave[2]; extern int slave2master[2]; extern int CURRENT_DISPLAY_ID; @@ -1329,6 +1330,45 @@ static bool kvm_drm_compute_desktop_layout(const kvm_drm_output *outputs, int ou return true; } +static void kvm_drm_publish_monitor_layout(const kvm_drm_output *outputs, int output_count, const kvm_drm_desktop_layout *layout) +{ + kvm_monitor_info monitors[KVM_MAX_MONITORS]; + int i; + int monitorCount = output_count; + + if (outputs == NULL || layout == NULL) + { + kvm_update_monitor_layout(NULL, 0, 0, 0); + return; + } + if (monitorCount > KVM_MAX_MONITORS) { monitorCount = KVM_MAX_MONITORS; } + + memset(monitors, 0, sizeof(monitors)); + for (i = 0; i < monitorCount; ++i) + { + monitors[i].id = i + 1; + monitors[i].x = outputs[i].x - layout->min_x; + monitors[i].y = outputs[i].y - layout->min_y; + monitors[i].width = (int)outputs[i].width; + monitors[i].height = (int)outputs[i].height; + } + kvm_update_monitor_layout(monitors, monitorCount, (int)layout->width, (int)layout->height); +} + +static void kvm_drm_update_tile_geometry() +{ + TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; + TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; + if (SCREEN_WIDTH % TILE_WIDTH) + { + TILE_WIDTH_COUNT++; + } + if (SCREEN_HEIGHT % TILE_HEIGHT) + { + TILE_HEIGHT_COUNT++; + } +} + static int kvm_drm_find_output_by_name(const kvm_drm_output *outputs, int output_count, const char *name) { int i; @@ -2042,6 +2082,9 @@ void *kvm_server_mainloop_drm(void *parm) int lastCaptureError = 0; int scanoutSuspended = 0; int forceTileReset = 0; + int reportedScreenWidth = 0; + int reportedScreenHeight = 0; + int reportedScreenSel = -1; uint64_t lastOutputRefreshMs = 0; uint64_t lastRefreshFailureLogMs = 0; char lastRefreshFailure[KVM_DRM_MAX_ERROR]; @@ -2152,6 +2195,8 @@ void *kvm_server_mainloop_drm(void *parm) return (void *)-1; } } + kvm_drm_publish_monitor_layout(outputs, outputCount, &layout); + kvm_drm_update_tile_geometry(); while (!g_shutdown) { @@ -2207,6 +2252,15 @@ void *kvm_server_mainloop_drm(void *parm) } } + if (change_display) + { + SCREEN_SEL = SCREEN_SEL_TARGET; + kvm_apply_monitor_selection(); + change_display = 0; + forceTileReset = 1; + displayListSent = 0; + } + uint64_t nowMs = kvm_drm_now_ms(); uint64_t frameInterval = FRAME_RATE_TIMER < 20 ? 20 : (uint64_t)FRAME_RATE_TIMER; if (nowMs == 0 || (lastFrameTimeMs != 0 && nowMs - lastFrameTimeMs < frameInterval)) @@ -2243,7 +2297,9 @@ void *kvm_server_mainloop_drm(void *parm) memcpy_s(outputs, sizeof(outputs), refreshed, sizeof(kvm_drm_output) * (size_t)refreshedCount); outputCount = refreshedCount; layout = refreshedLayout; + kvm_drm_publish_monitor_layout(outputs, outputCount, &layout); forceTileReset = 1; + displayListSent = 0; kvm_drm_destroy_map(&map); kvm_drm_egl_destroy_context(&eglCtx); kvm_drm_reset_logged_scanout_state(&lastLoggedFbId, &lastLoggedWidth, &lastLoggedHeight, @@ -2272,7 +2328,7 @@ void *kvm_server_mainloop_drm(void *parm) lastOutputRefreshMs = nowMs; } - size_t desktopRgbSize = (size_t)layout.width * (size_t)layout.height * 3u; + size_t desktopRgbSize = (size_t)SCREEN_WIDTH * (size_t)SCREEN_HEIGHT * 3u; if (desktopRgbBufferSize < desktopRgbSize) { unsigned char *tmp = (unsigned char *)realloc(desktopRgbBuffer, desktopRgbSize); @@ -2410,8 +2466,8 @@ void *kvm_server_mainloop_drm(void *parm) rgbFrame = rgbRotatedBuffer; } - kvm_drm_copy_frame_to_desktop(rgbFrame, effectiveWidth, effectiveHeight, desktopRgbBuffer, layout.width, layout.height, - outputs[outputIndex].x - layout.min_x, outputs[outputIndex].y - layout.min_y, + kvm_drm_copy_frame_to_desktop(rgbFrame, effectiveWidth, effectiveHeight, desktopRgbBuffer, (uint32_t)SCREEN_WIDTH, (uint32_t)SCREEN_HEIGHT, + outputs[outputIndex].x - layout.min_x - CAPTURE_X, outputs[outputIndex].y - layout.min_y - CAPTURE_Y, outputs[outputIndex].width, outputs[outputIndex].height); capturedOutputs++; } @@ -2427,25 +2483,17 @@ void *kvm_server_mainloop_drm(void *parm) scanoutSuspended = 0; lastCaptureError = 0; - if (SCREEN_WIDTH != (int)layout.width || SCREEN_HEIGHT != (int)layout.height) + if (SCREEN_WIDTH != reportedScreenWidth || SCREEN_HEIGHT != reportedScreenHeight || SCREEN_SEL != reportedScreenSel) { int oldTileHeightCount = TILE_HEIGHT_COUNT; - SCREEN_WIDTH = (int)layout.width; - SCREEN_HEIGHT = (int)layout.height; - TILE_HEIGHT_COUNT = SCREEN_HEIGHT / TILE_HEIGHT; - TILE_WIDTH_COUNT = SCREEN_WIDTH / TILE_WIDTH; - if (SCREEN_WIDTH % TILE_WIDTH) - { - TILE_WIDTH_COUNT++; - } - if (SCREEN_HEIGHT % TILE_HEIGHT) - { - TILE_HEIGHT_COUNT++; - } + kvm_drm_update_tile_geometry(); kvm_send_resolution(); kvm_send_display(); reset_tile_info(oldTileHeightCount); forceTileReset = 0; + reportedScreenWidth = SCREEN_WIDTH; + reportedScreenHeight = SCREEN_HEIGHT; + reportedScreenSel = SCREEN_SEL; } if (!displayListSent) From 0dbc8d740a3a9db211f9d91eb579eaafce6e2cfa Mon Sep 17 00:00:00 2001 From: Patrick O'Connell Date: Sun, 14 Jun 2026 01:39:11 +1000 Subject: [PATCH 13/14] Implement dynamic `dlopen` loading for libdrm, libEGL, and libGLESv2 Enable runtime loading of required libraries (libdrm, libEGL, and libGLESv2) using `dlopen` to allow the agent to function without a pre-installed GL/DRM stack. Refactor the makefile to remove hard library linking while retaining headers for build compatibility. Additionally, update Wayland integration to dynamically load libwayland-client and provide fallbacks for missing libraries. --- makefile | 18 ++- meshcore/KVM/Linux/linux_kvm_drm.c | 152 +++++++++++++++++++++++-- meshcore/KVM/Linux/linux_kvm_drm.h | 3 + meshcore/KVM/Linux/linux_kvm_drm_egl.c | 105 ++++++++++++++++- microscript/ILibDuktape_Commit.h | 4 +- 5 files changed, 269 insertions(+), 13 deletions(-) diff --git a/makefile b/makefile index 5e31e3579..29a6e2a41 100644 --- a/makefile +++ b/makefile @@ -551,7 +551,9 @@ LINUXKVMSOURCES = meshcore/KVM/Linux/linux_kvm.c meshcore/KVM/Linux/linux_kvm_wa MACOSKVMSOURCES = meshcore/KVM/MacOS/mac_kvm.c meshcore/KVM/MacOS/mac_events.c meshcore/KVM/MacOS/mac_tile.c meshcore/KVM/Linux/linux_compression.c CFLAGS += -D_LINKVM DRMCFLAGS = $(shell pkg-config --cflags libdrm egl glesv2 2>/dev/null) - DRMLIBS = $(shell pkg-config --libs libdrm egl glesv2 2>/dev/null) + # libdrm/libEGL/libGLESv2 are dlopen'd at runtime (linux_kvm_drm*.c), not linked, so they stay + # out of NEEDED and the agent runs without a DRM/GL stack. Headers still needed (DRMCFLAGS). + DRMLIBS = WAYLANDCLIENT = $(shell pkg-config --exists wayland-client 2>/dev/null && echo 1) ifneq ($(strip $(DRMCFLAGS)),) CFLAGS += $(DRMCFLAGS) @@ -560,7 +562,19 @@ CFLAGS += -D_LINKVM $(error Linux KVM builds require the wayland-client development package) endif CFLAGS += -DHAVE_WAYLAND_CLIENT $(shell pkg-config --cflags wayland-client) - DRMLIBS += $(shell pkg-config --libs wayland-client) + # libwayland-client is dlopen'd at runtime (linux_kvm_drm.c), not linked — headers only. + # If the system headers are jpeg8 (JPEG_LIB_VERSION >= 80) and a v80 lib exists, default to it + # so the linked lib matches the headers, else libjpeg aborts at runtime ("Wrong JPEG library + # version"). Skipped when JPEGVER is set explicitly or NOTURBOJPEG=1. + ifeq ($(JPEGVER),) + ifneq ($(NOTURBOJPEG),1) + JPEG_HDR_VERSION := $(shell $(CC) -include jpeglib.h -E -dM -xc - /dev/null | sed -n 's/.*JPEG_LIB_VERSION \([0-9][0-9]*\).*/\1/p' | head -n1) + ifeq ($(shell test "$(JPEG_HDR_VERSION)" -ge 80 2>/dev/null && test -f lib-jpeg-turbo/linux/$(ARCHNAME)/v80/libturbojpeg.a && echo yes),yes) + JPEGVER = v80 +$(info MeshAgent: system libjpeg is v$(JPEG_HDR_VERSION) (jpeg8); auto-selecting JPEGVER=v80) + endif + endif + endif ifneq ($(JPEGVER),) ifeq ($(LEGACY_LD),1) LINUXFLAGS = lib-jpeg-turbo/linux/$(ARCHNAME)/$(JPEGVER)/libturbojpeg.a diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index e743dc4b8..a6739356e 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -47,12 +47,135 @@ limitations under the License. #include #include +#include #include #include #include #include +// libdrm is dlopen'd rather than linked, so the agent still runs where it's absent (like libX11). +#define KVM_DRM_LIBDRM_SYMBOLS(_) \ + _(drmModeGetResources) _(drmModeFreeResources) \ + _(drmModeGetConnector) _(drmModeFreeConnector) \ + _(drmModeGetEncoder) _(drmModeFreeEncoder) \ + _(drmModeGetCrtc) _(drmModeFreeCrtc) \ + _(drmModeGetPlane) _(drmModeFreePlane) \ + _(drmModeGetPlaneResources) _(drmModeFreePlaneResources) \ + _(drmModeGetFB) _(drmModeFreeFB) \ + _(drmModeGetFB2) _(drmModeFreeFB2) \ + _(drmIoctl) _(drmPrimeHandleToFD) _(drmDropMaster) + +#define KVM_DRM_DECL_PTR(s) static __typeof__(s) *p_##s = NULL; +KVM_DRM_LIBDRM_SYMBOLS(KVM_DRM_DECL_PTR) +#undef KVM_DRM_DECL_PTR + +static void *g_libdrm_handle = NULL; +static int kvm_drm_load_libdrm(void) +{ + void *h; + if (g_libdrm_handle != NULL) { return 1; } + if ((h = dlopen("libdrm.so.2", RTLD_NOW)) == NULL && (h = dlopen("libdrm.so", RTLD_NOW)) == NULL) + { + return 0; + } +#define KVM_DRM_LOAD_PTR(s) p_##s = (__typeof__(p_##s))dlsym(h, #s); if (p_##s == NULL) { dlclose(h); return 0; } + KVM_DRM_LIBDRM_SYMBOLS(KVM_DRM_LOAD_PTR) +#undef KVM_DRM_LOAD_PTR + g_libdrm_handle = h; + return 1; +} + +// Must follow the __typeof__ declarations above (they need the real prototypes). +#define drmModeGetResources p_drmModeGetResources +#define drmModeFreeResources p_drmModeFreeResources +#define drmModeGetConnector p_drmModeGetConnector +#define drmModeFreeConnector p_drmModeFreeConnector +#define drmModeGetEncoder p_drmModeGetEncoder +#define drmModeFreeEncoder p_drmModeFreeEncoder +#define drmModeGetCrtc p_drmModeGetCrtc +#define drmModeFreeCrtc p_drmModeFreeCrtc +#define drmModeGetPlane p_drmModeGetPlane +#define drmModeFreePlane p_drmModeFreePlane +#define drmModeGetPlaneResources p_drmModeGetPlaneResources +#define drmModeFreePlaneResources p_drmModeFreePlaneResources +#define drmModeGetFB p_drmModeGetFB +#define drmModeFreeFB p_drmModeFreeFB +#define drmModeGetFB2 p_drmModeGetFB2 +#define drmModeFreeFB2 p_drmModeFreeFB2 +#define drmIoctl p_drmIoctl +#define drmPrimeHandleToFD p_drmPrimeHandleToFD +#define drmDropMaster p_drmDropMaster + +// Lets linux_kvm_drm_egl.c reach libdrm through the single dlopen here. +int kvm_drm_prime_handle_to_fd(int fd, unsigned int handle, unsigned int flags, int *prime_fd) +{ + return drmPrimeHandleToFD(fd, (uint32_t)handle, (uint32_t)flags, prime_fd); +} + +// libwayland-client is dlopen'd rather than linked, so the agent runs where it's absent. It's +// used only for the xdg-output layout query, which falls back to KWin/raw DRM positions. +#define KVM_DRM_WAYLAND_SYMBOLS(_) \ + _(wl_display_connect) _(wl_display_disconnect) _(wl_display_roundtrip) \ + _(wl_proxy_marshal_flags) _(wl_proxy_get_version) _(wl_proxy_add_listener) _(wl_proxy_destroy) + +#define KVM_DRM_WL_DECL_PTR(s) static __typeof__(s) *p_##s = NULL; +KVM_DRM_WAYLAND_SYMBOLS(KVM_DRM_WL_DECL_PTR) +#undef KVM_DRM_WL_DECL_PTR + +static const struct wl_interface *p_wl_registry_interface = NULL; +static const struct wl_interface *p_wl_output_interface = NULL; +static void *g_libwayland_handle = NULL; + +#define wl_display_connect p_wl_display_connect +#define wl_display_disconnect p_wl_display_disconnect +#define wl_display_roundtrip p_wl_display_roundtrip +#define wl_proxy_marshal_flags p_wl_proxy_marshal_flags +#define wl_proxy_get_version p_wl_proxy_get_version +#define wl_proxy_add_listener p_wl_proxy_add_listener +#define wl_proxy_destroy p_wl_proxy_destroy + +// Hand-rolled versions of the inline wrappers; the originals bake in +// link-time wl_proxy_*/wl_registry_interface references that would keep libwayland in NEEDED. +static struct wl_registry *kvm_wl_display_get_registry(struct wl_display *display) +{ + return (struct wl_registry *)wl_proxy_marshal_flags((struct wl_proxy *)display, 1 /*WL_DISPLAY_GET_REGISTRY*/, + p_wl_registry_interface, wl_proxy_get_version((struct wl_proxy *)display), 0, NULL); +} +static int kvm_wl_registry_add_listener(struct wl_registry *registry, const struct wl_registry_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *)registry, (void (**)(void))listener, data); +} +static void *kvm_wl_registry_bind(struct wl_registry *registry, uint32_t name, const struct wl_interface *interface, uint32_t version) +{ + return (void *)wl_proxy_marshal_flags((struct wl_proxy *)registry, 0 /*WL_REGISTRY_BIND*/, interface, version, 0, + name, interface->name, version, NULL); +} +static int kvm_wl_output_add_listener(struct wl_output *output, const struct wl_output_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *)output, (void (**)(void))listener, data); +} + +static const struct wl_interface *kvm_xdg_output_types[]; // defined below; slot [3] patched at load +static int kvm_drm_load_wayland(void) +{ + void *h; + if (g_libwayland_handle != NULL) { return 1; } + if ((h = dlopen("libwayland-client.so.0", RTLD_NOW)) == NULL && (h = dlopen("libwayland-client.so", RTLD_NOW)) == NULL) + { + return 0; + } +#define KVM_DRM_WL_LOAD_PTR(s) p_##s = (__typeof__(p_##s))dlsym(h, #s); if (p_##s == NULL) { dlclose(h); return 0; } + KVM_DRM_WAYLAND_SYMBOLS(KVM_DRM_WL_LOAD_PTR) +#undef KVM_DRM_WL_LOAD_PTR + p_wl_registry_interface = (const struct wl_interface *)dlsym(h, "wl_registry_interface"); + p_wl_output_interface = (const struct wl_interface *)dlsym(h, "wl_output_interface"); + if (p_wl_registry_interface == NULL || p_wl_output_interface == NULL) { dlclose(h); return 0; } + kvm_xdg_output_types[3] = p_wl_output_interface; // get_xdg_output's wl_output arg type + g_libwayland_handle = h; + return 1; +} + #ifndef O_CLOEXEC #define KVM_DRM_O_CLOEXEC 0 #else @@ -1419,7 +1542,6 @@ struct zxdg_output_v1_listener void (*description)(void *data, struct zxdg_output_v1 *zxdg_output_v1, const char *description); }; -extern const struct wl_interface wl_output_interface; static const struct wl_interface zxdg_output_v1_interface; static const struct wl_interface *kvm_xdg_output_types[] = @@ -1427,7 +1549,7 @@ static const struct wl_interface *kvm_xdg_output_types[] = NULL, NULL, &zxdg_output_v1_interface, - &wl_output_interface, + NULL, /* wl_output_interface — patched in at load */ }; static const struct wl_message zxdg_output_manager_v1_requests[] = @@ -1622,10 +1744,10 @@ static void kvm_drm_registry_global(void *data, struct wl_registry *registry, ui output->version = version; bind_version = version >= 4 ? 4 : version; if (bind_version < 2) { bind_version = 2; } - output->wl_output = (struct wl_output *)wl_registry_bind(registry, name, &wl_output_interface, bind_version); + output->wl_output = (struct wl_output *)kvm_wl_registry_bind(registry, name, p_wl_output_interface, bind_version); if (output->wl_output != NULL) { - wl_output_add_listener(output->wl_output, &kvm_drm_wl_output_listener, output); + kvm_wl_output_add_listener(output->wl_output, &kvm_drm_wl_output_listener, output); } return; } @@ -1633,7 +1755,7 @@ static void kvm_drm_registry_global(void *data, struct wl_registry *registry, ui if (strcmp(interface, "zxdg_output_manager_v1") == 0) { uint32_t bind_version = version >= 3 ? 3 : version; - ctx->xdg_output_manager = (struct zxdg_output_manager_v1 *)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, bind_version); + ctx->xdg_output_manager = (struct zxdg_output_manager_v1 *)kvm_wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, bind_version); } } @@ -1672,7 +1794,7 @@ static void kvm_drm_wayland_layout_context_cleanup(kvm_drm_wayland_layout_contex } if (ctx->registry != NULL) { - wl_registry_destroy(ctx->registry); + wl_proxy_destroy((struct wl_proxy *)ctx->registry); ctx->registry = NULL; } if (ctx->display != NULL) @@ -1694,6 +1816,11 @@ static bool kvm_drm_apply_xdg_output_layout(kvm_drm_output *outputs, int output_ return false; } + if (!kvm_drm_load_wayland()) + { + return false; // no libwayland → caller falls back to KWin/raw positions + } + memset(&ctx, 0, sizeof(ctx)); memcpy_s(tmp, sizeof(tmp), outputs, sizeof(kvm_drm_output) * (size_t)output_count); @@ -1702,13 +1829,13 @@ static bool kvm_drm_apply_xdg_output_layout(kvm_drm_output *outputs, int output_ { return false; } - ctx.registry = wl_display_get_registry(ctx.display); + ctx.registry = kvm_wl_display_get_registry(ctx.display); if (ctx.registry == NULL) { kvm_drm_wayland_layout_context_cleanup(&ctx); return false; } - wl_registry_add_listener(ctx.registry, &kvm_drm_registry_listener, &ctx); + kvm_wl_registry_add_listener(ctx.registry, &kvm_drm_registry_listener, &ctx); if (wl_display_roundtrip(ctx.display) < 0 || ctx.xdg_output_manager == NULL || ctx.output_count <= 0) { kvm_drm_wayland_layout_context_cleanup(&ctx); @@ -2151,6 +2278,15 @@ void *kvm_server_mainloop_drm(void *parm) map.dma_fd = -1; map.drm_fd = -1; + if (!kvm_drm_load_libdrm()) + { + kvm_send_error("libdrm is not installed; DRM capture backend unavailable"); + kvm_events_evdev_shutdown(); + g_enableEvents = 0; + g_kvmBackendDRM = 0; + return (void *)-1; + } + char *explicitDevice = getenv("MESH_KVM_DRM_DEVICE"); if (!kvm_drm_open_device_with_outputs(explicitDevice, &fd, outputs, KVM_DRM_MAX_OUTPUTS, &outputCount, err, sizeof(err)) || !kvm_drm_compute_desktop_layout(outputs, outputCount, &layout)) diff --git a/meshcore/KVM/Linux/linux_kvm_drm.h b/meshcore/KVM/Linux/linux_kvm_drm.h index 3a2f66770..1f319c768 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.h +++ b/meshcore/KVM/Linux/linux_kvm_drm.h @@ -25,4 +25,7 @@ extern int g_kvmBackendDRM; void* kvm_server_mainloop_drm(void* parm); +// Reaches libdrm's drmPrimeHandleToFD via the single dlopen in linux_kvm_drm.c. +int kvm_drm_prime_handle_to_fd(int fd, unsigned int handle, unsigned int flags, int *prime_fd); + #endif diff --git a/meshcore/KVM/Linux/linux_kvm_drm_egl.c b/meshcore/KVM/Linux/linux_kvm_drm_egl.c index d404dd209..58448d582 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm_egl.c +++ b/meshcore/KVM/Linux/linux_kvm_drm_egl.c @@ -15,6 +15,7 @@ limitations under the License. */ #include "linux_kvm_drm_egl.h" +#include "linux_kvm_drm.h" #include "meshcore/meshdefines.h" #include @@ -39,6 +40,102 @@ limitations under the License. #endif #endif +#if defined(__linux__) +#include + +// libEGL/libGLESv2 are dlopen'd rather than linked, so the agent runs without a GL stack. +// The *KHR/*OES extension entry points come from eglGetProcAddress, not this list. +#define KVM_DRM_EGL_SYMBOLS(_) \ + _(eglGetProcAddress) _(eglGetDisplay) _(eglGetError) _(eglInitialize) _(eglTerminate) \ + _(eglChooseConfig) _(eglCreateContext) _(eglDestroyContext) _(eglCreatePbufferSurface) \ + _(eglDestroySurface) _(eglMakeCurrent) _(eglBindAPI) _(eglQueryString) +#define KVM_DRM_GLES_SYMBOLS(_) \ + _(glActiveTexture) _(glAttachShader) _(glBindBuffer) _(glBindFramebuffer) _(glBindTexture) \ + _(glBufferData) _(glCheckFramebufferStatus) _(glCompileShader) _(glCreateProgram) _(glCreateShader) \ + _(glDeleteBuffers) _(glDeleteFramebuffers) _(glDeleteProgram) _(glDeleteShader) _(glDeleteTextures) \ + _(glDisableVertexAttribArray) _(glDrawArrays) _(glEnableVertexAttribArray) _(glFinish) \ + _(glFramebufferTexture2D) _(glGenBuffers) _(glGenFramebuffers) _(glGenTextures) _(glGetAttribLocation) \ + _(glGetError) _(glGetProgramiv) _(glGetShaderInfoLog) _(glGetShaderiv) _(glGetUniformLocation) \ + _(glLinkProgram) _(glReadPixels) _(glShaderSource) _(glTexImage2D) _(glTexParameteri) \ + _(glUniform1i) _(glUseProgram) _(glVertexAttribPointer) _(glViewport) + +#define KVM_DRM_EGL_DECL_PTR(s) static __typeof__(s) *p_##s = NULL; +KVM_DRM_EGL_SYMBOLS(KVM_DRM_EGL_DECL_PTR) +KVM_DRM_GLES_SYMBOLS(KVM_DRM_EGL_DECL_PTR) +#undef KVM_DRM_EGL_DECL_PTR + +static void *g_libEGL_handle = NULL; +static void *g_libGLES_handle = NULL; +static int kvm_drm_egl_load_libs(void) +{ + void *e, *g; + if (g_libEGL_handle != NULL && g_libGLES_handle != NULL) { return 1; } + if ((e = dlopen("libEGL.so.1", RTLD_NOW)) == NULL && (e = dlopen("libEGL.so", RTLD_NOW)) == NULL) { return 0; } + if ((g = dlopen("libGLESv2.so.2", RTLD_NOW)) == NULL && (g = dlopen("libGLESv2.so", RTLD_NOW)) == NULL) { dlclose(e); return 0; } +#define KVM_DRM_EGL_LOAD_PTR(s) p_##s = (__typeof__(p_##s))dlsym(e, #s); if (p_##s == NULL) { dlclose(e); dlclose(g); return 0; } + KVM_DRM_EGL_SYMBOLS(KVM_DRM_EGL_LOAD_PTR) +#undef KVM_DRM_EGL_LOAD_PTR +#define KVM_DRM_GLES_LOAD_PTR(s) p_##s = (__typeof__(p_##s))dlsym(g, #s); if (p_##s == NULL) { dlclose(e); dlclose(g); return 0; } + KVM_DRM_GLES_SYMBOLS(KVM_DRM_GLES_LOAD_PTR) +#undef KVM_DRM_GLES_LOAD_PTR + g_libEGL_handle = e; + g_libGLES_handle = g; + return 1; +} + +#define eglGetProcAddress p_eglGetProcAddress +#define eglGetDisplay p_eglGetDisplay +#define eglGetError p_eglGetError +#define eglInitialize p_eglInitialize +#define eglTerminate p_eglTerminate +#define eglChooseConfig p_eglChooseConfig +#define eglCreateContext p_eglCreateContext +#define eglDestroyContext p_eglDestroyContext +#define eglCreatePbufferSurface p_eglCreatePbufferSurface +#define eglDestroySurface p_eglDestroySurface +#define eglMakeCurrent p_eglMakeCurrent +#define eglBindAPI p_eglBindAPI +#define eglQueryString p_eglQueryString +#define glActiveTexture p_glActiveTexture +#define glAttachShader p_glAttachShader +#define glBindBuffer p_glBindBuffer +#define glBindFramebuffer p_glBindFramebuffer +#define glBindTexture p_glBindTexture +#define glBufferData p_glBufferData +#define glCheckFramebufferStatus p_glCheckFramebufferStatus +#define glCompileShader p_glCompileShader +#define glCreateProgram p_glCreateProgram +#define glCreateShader p_glCreateShader +#define glDeleteBuffers p_glDeleteBuffers +#define glDeleteFramebuffers p_glDeleteFramebuffers +#define glDeleteProgram p_glDeleteProgram +#define glDeleteShader p_glDeleteShader +#define glDeleteTextures p_glDeleteTextures +#define glDisableVertexAttribArray p_glDisableVertexAttribArray +#define glDrawArrays p_glDrawArrays +#define glEnableVertexAttribArray p_glEnableVertexAttribArray +#define glFinish p_glFinish +#define glFramebufferTexture2D p_glFramebufferTexture2D +#define glGenBuffers p_glGenBuffers +#define glGenFramebuffers p_glGenFramebuffers +#define glGenTextures p_glGenTextures +#define glGetAttribLocation p_glGetAttribLocation +#define glGetError p_glGetError +#define glGetProgramiv p_glGetProgramiv +#define glGetShaderInfoLog p_glGetShaderInfoLog +#define glGetShaderiv p_glGetShaderiv +#define glGetUniformLocation p_glGetUniformLocation +#define glLinkProgram p_glLinkProgram +#define glReadPixels p_glReadPixels +#define glShaderSource p_glShaderSource +#define glTexImage2D p_glTexImage2D +#define glTexParameteri p_glTexParameteri +#define glUniform1i p_glUniform1i +#define glUseProgram p_glUseProgram +#define glVertexAttribPointer p_glVertexAttribPointer +#define glViewport p_glViewport +#endif // __linux__ + static void kvm_drm_egl_copy_error_message(char *dst, size_t dst_size, const char *src) { if (dst == NULL || dst_size == 0) { return; } @@ -168,6 +265,12 @@ static bool kvm_drm_egl_init_gpu_readback(kvm_drm_egl_context *g, char *out_erro return false; } + if (!kvm_drm_egl_load_libs()) + { + return kvm_drm_egl_fail_with_persistent_error(g, out_error, out_error_size, + "libEGL/libGLESv2 not installed; GPU-assisted DRM conversion unavailable"); + } + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXTFn = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); if (eglGetPlatformDisplayEXTFn != NULL) @@ -431,7 +534,7 @@ bool kvm_drm_egl_convert_to_rgb24_gpu(kvm_drm_egl_context *ctx, int drm_fd, uint if (!kvm_drm_egl_ensure_gpu_target_size(ctx, (int)width, (int)height, out_error, out_error_size)) { return false; } int dmabuf_fd = -1; - if (drmPrimeHandleToFD(drm_fd, handle, KVM_DRM_EGL_CLOEXEC | DRM_RDWR, &dmabuf_fd) != 0 || dmabuf_fd < 0) + if (kvm_drm_prime_handle_to_fd(drm_fd, handle, KVM_DRM_EGL_CLOEXEC | DRM_RDWR, &dmabuf_fd) != 0 || dmabuf_fd < 0) { char msg[KVM_DRM_EGL_MAX_ERROR]; snprintf(msg, sizeof(msg), "drmPrimeHandleToFD failed for GPU path (handle=%u errno=%d)", handle, errno); diff --git a/microscript/ILibDuktape_Commit.h b/microscript/ILibDuktape_Commit.h index e99806709..1cb46bd76 100644 --- a/microscript/ILibDuktape_Commit.h +++ b/microscript/ILibDuktape_Commit.h @@ -1,3 +1,3 @@ // This file is auto-generated, any edits may be overwritten -#define SOURCE_COMMIT_DATE "2026-May-26 08:29:16+1000" -#define SOURCE_COMMIT_HASH "ee8aa56ea2873c1ff97918380bf43568b256ea3c" +#define SOURCE_COMMIT_DATE "2026-Jun-5 08:47:50+1000" +#define SOURCE_COMMIT_HASH "f7e88b6a4b86838e61e5515a553979cc9423604e" From a6ef06d18120e1f5a501016b27a9034e07814916 Mon Sep 17 00:00:00 2001 From: Patrick O'Connell Date: Sun, 14 Jun 2026 11:59:17 +1000 Subject: [PATCH 14/14] Add display wake and broken-pipe handling improvements for DRM and KVM Introduce logic to nudge sleeping displays (DPMS) awake during DRM desktop layout calculations and retry within a timeout. Enhance KVM pipe cleanup by canceling timers and broken-pipe handlers to prevent errors from delayed memory access. Define `KVM_DRM_DISPLAY_WAKE_TIMEOUT_MS` and implement `kvm_events_evdev_wake` for pointer-based wake functionality. --- meshcore/KVM/Linux/linux_events.h | 1 + meshcore/KVM/Linux/linux_events_evdev.c | 14 ++++++++++++++ meshcore/KVM/Linux/linux_kvm.c | 1 + meshcore/KVM/Linux/linux_kvm_drm.c | 19 +++++++++++++++++-- meshcore/agentcore.c | 16 ++++++++++++++-- microscript/ILibDuktape_Commit.h | 4 ++-- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/meshcore/KVM/Linux/linux_events.h b/meshcore/KVM/Linux/linux_events.h index 46b458991..b970650d3 100644 --- a/meshcore/KVM/Linux/linux_events.h +++ b/meshcore/KVM/Linux/linux_events.h @@ -442,6 +442,7 @@ extern int kvm_events_evdev_is_active(); extern void kvm_events_evdev_mouse_action(double absX, double absY, int button, short wheel); extern void kvm_events_evdev_key_action(unsigned char vk, int up); extern void kvm_events_evdev_key_action_unicode(uint16_t unicode, int up); +extern void kvm_events_evdev_wake(); extern void MouseAction(double absX, double absY, int button, short wheel, Display *display); extern void KeyAction(unsigned char vk, int up, Display *display); diff --git a/meshcore/KVM/Linux/linux_events_evdev.c b/meshcore/KVM/Linux/linux_events_evdev.c index c37bae936..5ff94a9f3 100644 --- a/meshcore/KVM/Linux/linux_events_evdev.c +++ b/meshcore/KVM/Linux/linux_events_evdev.c @@ -822,6 +822,16 @@ void kvm_events_evdev_mouse_action(double absX, double absY, int button, short w ignore_result(kvm_events_evdev_sync()); } +// Net-zero scroll: pointer activity that wakes the displays (DPMS) without side effects. +void kvm_events_evdev_wake() +{ + if (g_kvm_evdev_state.active == 0) { return; } + if (kvm_events_evdev_write(EV_REL, REL_WHEEL, 1) != 0) { return; } + ignore_result(kvm_events_evdev_sync()); + if (kvm_events_evdev_write(EV_REL, REL_WHEEL, -1) != 0) { return; } + ignore_result(kvm_events_evdev_sync()); +} + void kvm_events_evdev_key_action(unsigned char vk, int up) { unsigned int keycode = kvm_events_evdev_vk_to_keycode(vk); @@ -914,6 +924,10 @@ void kvm_events_evdev_mouse_action(double absX, double absY, int button, short w UNREFERENCED_PARAMETER(wheel); } +void kvm_events_evdev_wake() +{ +} + void kvm_events_evdev_key_action(unsigned char vk, int up) { UNREFERENCED_PARAMETER(vk); diff --git a/meshcore/KVM/Linux/linux_kvm.c b/meshcore/KVM/Linux/linux_kvm.c index 07b1e352e..ac9f04979 100644 --- a/meshcore/KVM/Linux/linux_kvm.c +++ b/meshcore/KVM/Linux/linux_kvm.c @@ -1633,6 +1633,7 @@ void kvm_relay_readSink(ILibProcessPipe_Pipe sender, char *buffer, size_t buffer void kvm_relay_brokenPipeSink_2(void *sender) { + if (!ILibMemory_CanaryOK(sender)) { return; } // pipe was freed before this 4s timer fired ILibKVM_WriteHandler writeHandler = (ILibKVM_WriteHandler)((void**)ILibMemory_Extra(sender))[0]; void *reserved = ((void**)ILibMemory_Extra(sender))[1]; char msg[] = "KVM Child process has unexpectedly exited"; diff --git a/meshcore/KVM/Linux/linux_kvm_drm.c b/meshcore/KVM/Linux/linux_kvm_drm.c index a6739356e..96e7693db 100644 --- a/meshcore/KVM/Linux/linux_kvm_drm.c +++ b/meshcore/KVM/Linux/linux_kvm_drm.c @@ -185,6 +185,7 @@ static int kvm_drm_load_wayland(void) #define KVM_DRM_MAX_ERROR 256 #define KVM_DRM_MAX_OUTPUTS 16 +#define KVM_DRM_DISPLAY_WAKE_TIMEOUT_MS 10000 int g_kvmBackendDRM = 0; static int drm_debug = 0; @@ -2288,8 +2289,22 @@ void *kvm_server_mainloop_drm(void *parm) } char *explicitDevice = getenv("MESH_KVM_DRM_DEVICE"); - if (!kvm_drm_open_device_with_outputs(explicitDevice, &fd, outputs, KVM_DRM_MAX_OUTPUTS, &outputCount, err, sizeof(err)) || - !kvm_drm_compute_desktop_layout(outputs, outputCount, &layout)) + int haveLayout = kvm_drm_open_device_with_outputs(explicitDevice, &fd, outputs, KVM_DRM_MAX_OUTPUTS, &outputCount, err, sizeof(err)) && + kvm_drm_compute_desktop_layout(outputs, outputCount, &layout); + if (!haveLayout) + { + // No scanout usually means the displays are in DPMS sleep; nudge them awake and retry. + uint64_t wakeStartMs = kvm_drm_now_ms(); + if (drm_debug) { fprintf(stderr, "DRM: no active scanout (%s); nudging displays awake and retrying\n", err[0] ? err : "displays asleep?"); } + while (!haveLayout && !g_shutdown && (kvm_drm_now_ms() - wakeStartMs) < KVM_DRM_DISPLAY_WAKE_TIMEOUT_MS) + { + if (g_enableEvents) { kvm_events_evdev_wake(); } + usleep(500 * 1000); + haveLayout = kvm_drm_open_device_with_outputs(explicitDevice, &fd, outputs, KVM_DRM_MAX_OUTPUTS, &outputCount, err, sizeof(err)) && + kvm_drm_compute_desktop_layout(outputs, outputCount, &layout); + } + } + if (!haveLayout) { kvm_send_error(err[0] ? err : "Unable to compute DRM desktop layout"); kvm_events_evdev_shutdown(); diff --git a/meshcore/agentcore.c b/meshcore/agentcore.c index 80ef18836..77f5553ea 100644 --- a/meshcore/agentcore.c +++ b/meshcore/agentcore.c @@ -980,7 +980,13 @@ void ILibDuktape_MeshAgent_RemoteDesktop_EndSink(ILibDuktape_DuplexStream *strea duk_del_prop_string(ptrs->ctx, -1, REMOTE_DESKTOP_STREAM); duk_pop(ptrs->ctx); // ... #if defined(_LINKVM) && defined(_POSIX) && !defined(__APPLE__) - if (ptrs->kvmPipe != NULL) { ILibProcessPipe_FreePipe(ptrs->kvmPipe); } + if (ptrs->kvmPipe != NULL) + { + // Cancel the pending broken-pipe timer before freeing, else it fires ~4s later on freed memory. + ILibLifeTime_Remove(ILibGetBaseTimer(duk_ctx_chain(ptrs->ctx)), ptrs->kvmPipe); + ILibProcessPipe_Pipe_SetBrokenPipeHandler(ptrs->kvmPipe, NULL); + ILibProcessPipe_FreePipe(ptrs->kvmPipe); + } #endif memset(ptrs, 0, sizeof(RemoteDesktop_Ptrs)); } @@ -1042,7 +1048,13 @@ duk_ret_t ILibDuktape_MeshAgent_RemoteDesktop_Finalizer(duk_context *ctx) duk_pop(ptrs->ctx); // ... #ifdef _LINKVM #if defined(_POSIX) && !defined(__APPLE__) - if (ptrs->kvmPipe != NULL) { ILibProcessPipe_FreePipe(ptrs->kvmPipe); } + if (ptrs->kvmPipe != NULL) + { + // Cancel the pending broken-pipe timer before freeing, else it fires ~4s later on freed memory. + ILibLifeTime_Remove(ILibGetBaseTimer(duk_ctx_chain(ptrs->ctx)), ptrs->kvmPipe); + ILibProcessPipe_Pipe_SetBrokenPipeHandler(ptrs->kvmPipe, NULL); + ILibProcessPipe_FreePipe(ptrs->kvmPipe); + } #endif kvm_cleanup(); #endif diff --git a/microscript/ILibDuktape_Commit.h b/microscript/ILibDuktape_Commit.h index 1cb46bd76..8918402d4 100644 --- a/microscript/ILibDuktape_Commit.h +++ b/microscript/ILibDuktape_Commit.h @@ -1,3 +1,3 @@ // This file is auto-generated, any edits may be overwritten -#define SOURCE_COMMIT_DATE "2026-Jun-5 08:47:50+1000" -#define SOURCE_COMMIT_HASH "f7e88b6a4b86838e61e5515a553979cc9423604e" +#define SOURCE_COMMIT_DATE "2026-Jun-14 01:39:11+1000" +#define SOURCE_COMMIT_HASH "0dbc8d740a3a9db211f9d91eb579eaafce6e2cfa"