#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include "kmsvnc.h" #include "keymap.h" #include "input.h" #include "drm.h" #include "va.h" struct kmsvnc_data *kmsvnc = NULL; #define NS_IN_S 1000000000 static void between_frames() { static struct timespec now = {0, 0}, then = {0, 0}, tmp = {0, 0}; clock_gettime(CLOCK_MONOTONIC, &now); memcpy((char *)&then, (char *)&tmp, sizeof(struct timespec)); tmp.tv_nsec += kmsvnc->vnc_opt->sleep_ns; if (tmp.tv_nsec >= NS_IN_S) { tmp.tv_sec++; tmp.tv_nsec %= NS_IN_S; } if (now.tv_sec < tmp.tv_sec || (now.tv_sec == tmp.tv_sec && now.tv_nsec < tmp.tv_nsec)) { then.tv_sec = tmp.tv_sec - now.tv_sec; then.tv_nsec = tmp.tv_nsec - now.tv_nsec; if (then.tv_nsec < 0) { then.tv_sec--; then.tv_nsec += NS_IN_S; } nanosleep(&then, &then); } memcpy((char *)&now, (char *)&then, sizeof(struct timespec)); } static void update_screen_buf(char* to, char *from, int width, int height) { uint64_t *double_pix_from = (uint64_t *)from; uint64_t *double_pix_to = (uint64_t *)to; int min_x = INT32_MAX; int min_y = INT32_MAX; int max_x = -1; int max_y = -1; if (!kmsvnc->vnc_opt->disable_cmpfb && width % 2 == 0) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x+=2) { if (*double_pix_from != *double_pix_to) { if (x < min_x) { min_x = x; } if (x > max_x) { max_x = x; } if (y < min_y) { min_y = y; } if (y > max_y) { max_y = y; } } double_pix_from ++; double_pix_to ++; } } } else { memcpy(to, from, width * height * BYTES_PER_PIXEL); rfbMarkRectAsModified(kmsvnc->server, 0, 0, width, height); return; } max_x = max_x < 0 ? 0 : max_x; max_y = max_y < 0 ? 0 : max_y; min_x = min_x > width ? 0 : min_x; min_y = min_y > height ? 0 : min_y; //printf("dirty: %d, %d, %d, %d\n", min_x, min_y, max_x, max_y); if (max_x || max_y || min_x || min_y) { memcpy(to, from, width * height * BYTES_PER_PIXEL); rfbMarkRectAsModified(kmsvnc->server, min_x, min_y, max_x + 2, max_y + 1); } } static inline void update_vnc_cursor(char *data, int width, int height) { uint8_t r, g, b, a; #define CURSOR_MIN_A 160 // ~63% int min_x = width; int max_x = -1; int min_y = height; int max_y = -1; int x, y; for (int i = 0; i < width * height * BYTES_PER_PIXEL; i += BYTES_PER_PIXEL) { uint32_t pixdata = htonl(*((uint32_t*)(data + i))); //r = (pixdata & 0xff000000u) >> 24; //g = (pixdata & 0x00ff0000u) >> 16; //b = (pixdata & 0x0000ff00u) >> 8; a = pixdata & 0xff; if (a > CURSOR_MIN_A) { x = (i / BYTES_PER_PIXEL) % width; y = (i / BYTES_PER_PIXEL) / width; if (x < min_x) min_x = x; if (y < min_y) min_y = y; if (x > max_x) max_x = x; if (y > max_y) max_y = y; } } if (min_x > max_x || min_y > max_y) { // no cursor detected return; } int rwidth = max_x - min_x + 1; int rheight = max_y - min_y + 1; if (kmsvnc->cursor_bitmap_len < rwidth * rheight * BYTES_PER_PIXEL) { if (kmsvnc->cursor_bitmap) free(kmsvnc->cursor_bitmap); kmsvnc->cursor_bitmap = malloc(rwidth * rheight * BYTES_PER_PIXEL); if (!kmsvnc->cursor_bitmap) return; kmsvnc->cursor_bitmap_len = rwidth * rheight * BYTES_PER_PIXEL; } unsigned char *rich_source = malloc(rwidth * rheight * BYTES_PER_PIXEL); if (!rich_source) return; char *maskString = malloc(rwidth * rheight); if (!maskString) { free(rich_source); return; } memset(maskString, ' ', rwidth * rheight); for (int i = 0; i < rwidth; i++) { for (int j = 0; j < rheight; j++) { int t = (i + j * rwidth) * BYTES_PER_PIXEL; int s = ((i+min_x) + (j+min_y) * width) * BYTES_PER_PIXEL; *((uint32_t*)(rich_source + t)) = *((uint32_t*)(data + s)); if ((uint8_t)*(rich_source + t + 3) > CURSOR_MIN_A) { maskString[i + j * rwidth] = 'x'; } } } if ((kmsvnc->server->cursor->width != rwidth || kmsvnc->server->cursor->height != rheight) || memcmp(kmsvnc->cursor_bitmap, rich_source, rwidth * rheight * BYTES_PER_PIXEL)) { KMSVNC_DEBUG("cursor update %dx%d\n", rwidth, rheight); memcpy(kmsvnc->cursor_bitmap, rich_source, kmsvnc->cursor_bitmap_len); char *cursorString = malloc(rwidth * rheight); if (!cursorString) { free(rich_source); free(maskString); return; } memset(cursorString, 'x', rwidth * rheight); rfbCursorPtr cursor = rfbMakeXCursor(rwidth, rheight, cursorString, maskString); free(cursorString); cursor->richSource = rich_source; cursor->cleanupRichSource = TRUE; cursor->xhot = 0; cursor->yhot = 0; rfbSetCursor(kmsvnc->server, cursor); } else { free(rich_source); free(maskString); } } static void cleanup() { if (kmsvnc->keymap) { xkb_cleanup(); } if (kmsvnc->input) { uinput_cleanup(); } if (kmsvnc->drm) { drm_cleanup(); } if (kmsvnc->va) { va_cleanup(); } if (kmsvnc) { if (kmsvnc->vnc_opt) { free(kmsvnc->vnc_opt); kmsvnc->vnc_opt = NULL; } if (kmsvnc->buf1) { free(kmsvnc->buf1); kmsvnc->buf1 = NULL; } if (kmsvnc->buf) { free(kmsvnc->buf); kmsvnc->buf = NULL; } if (kmsvnc->cursor_bitmap) { free(kmsvnc->cursor_bitmap); kmsvnc->cursor_bitmap = NULL; } kmsvnc->cursor_bitmap_len = 0; free(kmsvnc); kmsvnc = NULL; } } void signal_handler_noop(int signum){} void signal_handler(int signum){ if (kmsvnc->shutdown) { return; } kmsvnc->shutdown = 1; if (kmsvnc->server) { rfbShutdownServer(kmsvnc->server,TRUE); } } static struct argp_option kmsvnc_main_options[] = { {"device", 'd', "/dev/dri/cardX", 0, "DRM device"}, {"source-plane", 0xfefc, "0", 0, "Use specific plane"}, {"source-crtc", 0xfefd, "0", 0, "Use specific crtc (to list all crtcs and planes, set this to -1)"}, {"force-driver", 0xfefe, "i915", 0, "force a certain driver (for debugging)"}, {"bind", 'b', "0.0.0.0", 0, "Listen on (ipv4 address)"}, {"bind6", 0xfeff, "::", 0, "Listen on (ipv6 address)"}, {"port", 'p', "5900", 0, "Listen port"}, {"disable-ipv6", '4', 0, OPTION_ARG_OPTIONAL, "Disable ipv6"}, {"fps", 0xff00, "30", 0, "Target frames per second"}, {"disable-always-shared", 0xff01, 0, OPTION_ARG_OPTIONAL, "Do not always treat incoming connections as shared"}, {"disable-compare-fb", 0xff02, 0, OPTION_ARG_OPTIONAL, "Do not compare pixels"}, {"capture-cursor", 'c', 0, OPTION_ARG_OPTIONAL, "Capture mouse cursor"}, {"capture-raw-fb", 0xff03, "/tmp/rawfb.bin", 0, "Capture RAW framebuffer instead of starting the vnc server (for debugging)"}, {"va-derive", 0xff04, "off", 0, "Enable derive with vaapi"}, {"debug", 0xff05, 0, OPTION_ARG_OPTIONAL, "Print debug message"}, {"input-width", 0xff06, "0", 0, "Explicitly set input width, normally this is inferred from screen width on a single display system"}, {"input-height", 0xff07, "0", 0, "Explicitly set input height"}, {"input-offx", 0xff08, "0", 0, "Set input offset of x axis on a multi display system"}, {"input-offy", 0xff09, "0", 0, "Set input offset of y axis on a multi display system"}, #ifndef DISABLE_KMSVNC_SCREEN_BLANK {"screen-blank", 0xff0a, 0, OPTION_ARG_OPTIONAL, "Blank screen with gamma set on crtc"}, {"screen-blank-restore-linear", 0xff0b, 0, OPTION_ARG_OPTIONAL, "Restore linear values on exit in case of messed up gamma"}, #endif {"va-byteorder-swap", 0xff0c, 0, OPTION_ARG_OPTIONAL, "Force swap vaapi image rgb byteorder"}, {"wakeup", 'w', 0, OPTION_ARG_OPTIONAL, "Move mouse to wake the system up before start"}, {"disable-input", 'i', 0, OPTION_ARG_OPTIONAL, "Disable uinput"}, {"desktop-name", 'n', "kmsvnc", 0, "Specify vnc desktop name"}, {0} }; static error_t parse_opt(int key, char *arg, struct argp_state *state) { int *arg_cout = state->input; switch (key) { case 'd': kmsvnc->card = arg; break; case 0xfefc: kmsvnc->source_plane = atoi(arg); break; case 0xfefd: kmsvnc->source_crtc = atoi(arg); break; case 0xfefe: kmsvnc->force_driver = arg; break; case 'b': if (!inet_aton(arg, kmsvnc->vnc_opt->bind)) { argp_error(state, "invalid ipv4 address %s", arg); } break; case 0xfeff: kmsvnc->vnc_opt->bind6 = arg; break; case 'p': { int port = atoi(arg); if (port > 0 && port < 65536) { kmsvnc->vnc_opt->port = port; } else { argp_error(state, "invalid port %s", arg); } } break; case '4': kmsvnc->vnc_opt->disable_ipv6 = 1; break; case 0xff00: { int fps = atoi(arg); if (fps > 0 && fps < 1000) { kmsvnc->vnc_opt->sleep_ns = NS_IN_S / fps; } else { argp_error(state, "invalid fps %s", arg); } } break; case 0xff01: kmsvnc->vnc_opt->always_shared = 0; break; case 0xff02: kmsvnc->vnc_opt->disable_cmpfb = 1; break; case 'c': kmsvnc->capture_cursor = 1; break; case 0xff03: kmsvnc->debug_capture_fb = arg; kmsvnc->disable_input = 1; break; case 0xff04: if (!strcmp("on", arg) || !strcmp("y", arg) || !strcmp("yes", arg) || !strcmp("1", arg)) { kmsvnc->va_derive_enabled = 1; } else { kmsvnc->va_derive_enabled = 0; } break; case 0xff05: kmsvnc->debug_enabled = 1; break; case 0xff06: { int width = atoi(arg); if (width > 0) { kmsvnc->input_width = width; } } break; case 0xff07: { int height = atoi(arg); if (height > 0) { kmsvnc->input_height = height; } } break; case 0xff08: { int offset_x = atoi(arg); if (offset_x > 0) { kmsvnc->input_offx = offset_x; } } break; case 0xff09: { int offset_y = atoi(arg); if (offset_y > 0) { kmsvnc->input_offy = offset_y; } } break; case 0xff0a: kmsvnc->screen_blank = 1; break; case 0xff0b: kmsvnc->screen_blank_restore = 1; break; case 0xff0c: kmsvnc->va_byteorder_swap = 1; break; case 'w': kmsvnc->input_wakeup = 1; break; case 'i': kmsvnc->disable_input = 1; break; case 'n': kmsvnc->vnc_opt->desktop_name = arg; break; case ARGP_KEY_ARG: return ARGP_ERR_UNKNOWN; default: return ARGP_ERR_UNKNOWN; } return 0; } int main(int argc, char **argv) { kmsvnc = malloc(sizeof(struct kmsvnc_data)); if (!kmsvnc) KMSVNC_FATAL("memory allocation error at %s:%d\n", __FILE__, __LINE__); memset(kmsvnc, 0, sizeof(struct kmsvnc_data)); struct vnc_opt *vncopt = malloc(sizeof(struct vnc_opt)); if (!vncopt) { free(kmsvnc); KMSVNC_FATAL("memory allocation error at %s:%d\n", __FILE__, __LINE__); } memset(vncopt, 0, sizeof(struct vnc_opt)); kmsvnc->vnc_opt = vncopt; #define DEVICE_EXAMPLE_MAX_SIZE 15 #define DEVICE_EXAMPLE_FALLBACK "/dev/dri/card0" static char device_example[DEVICE_EXAMPLE_MAX_SIZE] = DEVICE_EXAMPLE_FALLBACK; kmsvnc->card = device_example; kmsvnc->va_derive_enabled = -1; kmsvnc->vnc_opt->bind = &(struct in_addr){0}; kmsvnc->vnc_opt->always_shared = 1; kmsvnc->vnc_opt->port = 5900; kmsvnc->vnc_opt->sleep_ns = NS_IN_S / 30; kmsvnc->vnc_opt->desktop_name = "kmsvnc"; static char *args_doc = ""; static char *doc = "kmsvnc -- vncserver for DRM/KMS capable GNU/Linux devices"; struct argp argp = {kmsvnc_main_options, parse_opt, args_doc, doc}; argp_parse(&argp, argc, argv, 0, 0, NULL); if (kmsvnc->card == device_example) { for (int i = 0; i < 10; i++) { snprintf(kmsvnc->card, DEVICE_EXAMPLE_MAX_SIZE, "/dev/dri/card%d", i); if (!access(kmsvnc->card, F_OK)) { break; } else { snprintf(kmsvnc->card, DEVICE_EXAMPLE_MAX_SIZE, DEVICE_EXAMPLE_FALLBACK); } } } if (!kmsvnc->disable_input) { const char* XKB_DEFAULT_LAYOUT = getenv("XKB_DEFAULT_LAYOUT"); if (!XKB_DEFAULT_LAYOUT || strcmp(XKB_DEFAULT_LAYOUT, "") == 0) { printf("No keyboard layout set from environment variables, use US layout by default\n"); printf("See https://xkbcommon.org/doc/current/structxkb__rule__names.html\n"); setenv("XKB_DEFAULT_LAYOUT", "us", 1); } if (xkb_init()) { cleanup(); return 1; } if (uinput_init()) { cleanup(); return 1; } } if (drm_open()) { cleanup(); return 1; } if (kmsvnc->debug_capture_fb) { int wfd = open(kmsvnc->debug_capture_fb, O_WRONLY | O_CREAT, 00644); int max_size = 0; for (int i = 0; i < 4; i++) { int size = kmsvnc->drm->mfb->offsets[i] + kmsvnc->drm->mfb->height * kmsvnc->drm->mfb->pitches[i]; if (size > max_size) max_size = size; } printf("attempt to write %d bytes\n", max_size); if (wfd > 0) { if (kmsvnc->va) { if (!kmsvnc->drm->mapped) kmsvnc->drm->mapped = malloc(max_size); if (!kmsvnc->drm->mapped) { cleanup(); KMSVNC_FATAL("memory allocation error at %s:%d\n", __FILE__, __LINE__); } va_hwframe_to_vaapi(kmsvnc->drm->mapped); } KMSVNC_WRITE_MAY(wfd, kmsvnc->drm->mapped, (ssize_t)max_size); fsync(wfd); printf("wrote raw frame buffer to %s\n", kmsvnc->debug_capture_fb); } else { fprintf(stderr, "open file %s failed, %s\n", kmsvnc->debug_capture_fb, strerror(errno)); } if (kmsvnc->screen_blank) { sigset_t signal_set; int sig; sigemptyset(&signal_set); signal(SIGHUP, &signal_handler_noop); signal(SIGINT, &signal_handler_noop); signal(SIGTERM, &signal_handler_noop); sigaddset(&signal_set, SIGHUP); sigaddset(&signal_set, SIGINT); sigaddset(&signal_set, SIGTERM); fprintf(stderr, "blanking screen...\n"); sigwait(&signal_set, &sig); fprintf(stderr, "got sig %d\n", sig); } cleanup(); return 0; } size_t buflen = kmsvnc->drm->mfb->width * kmsvnc->drm->mfb->height * BYTES_PER_PIXEL; kmsvnc->buf = malloc(buflen); if (!kmsvnc->buf) { cleanup(); KMSVNC_FATAL("memory allocation error at %s:%d\n", __FILE__, __LINE__); } memset(kmsvnc->buf, 0, buflen); kmsvnc->buf1 = malloc(buflen); if (!kmsvnc->buf1) { cleanup(); KMSVNC_FATAL("memory allocation error at %s:%d\n", __FILE__, __LINE__); } memset(kmsvnc->buf1, 0, buflen); signal(SIGHUP, &signal_handler); signal(SIGINT, &signal_handler); signal(SIGTERM, &signal_handler); kmsvnc->server = rfbGetScreen(0, NULL, kmsvnc->drm->mfb->width, kmsvnc->drm->mfb->height, 8, 3, 4); if (!kmsvnc->server) { cleanup(); return 1; } kmsvnc->server->desktopName = kmsvnc->vnc_opt->desktop_name; kmsvnc->server->frameBuffer = kmsvnc->buf; kmsvnc->server->port = kmsvnc->vnc_opt->port; kmsvnc->server->listenInterface = kmsvnc->vnc_opt->bind->s_addr; kmsvnc->server->ipv6port = kmsvnc->vnc_opt->disable_ipv6 ? 0 : kmsvnc->vnc_opt->port; kmsvnc->server->listen6Interface = kmsvnc->vnc_opt->bind6; kmsvnc->server->alwaysShared = kmsvnc->vnc_opt->always_shared; if (!kmsvnc->disable_input) { kmsvnc->server->kbdAddEvent = rfb_key_hook; kmsvnc->server->ptrAddEvent = rfb_ptr_hook; } rfbInitServer(kmsvnc->server); rfbRunEventLoop(kmsvnc->server, -1, TRUE); int cursor_frame = 0; while (rfbIsActive(kmsvnc->server)) { between_frames(); if (kmsvnc->server->clientHead) { kmsvnc->drm->funcs->sync_start(kmsvnc->drm->prime_fd); kmsvnc->drm->funcs->convert(kmsvnc->drm->mapped, kmsvnc->drm->mfb->width, kmsvnc->drm->mfb->height, kmsvnc->buf1); kmsvnc->drm->funcs->sync_end(kmsvnc->drm->prime_fd); update_screen_buf(kmsvnc->buf, kmsvnc->buf1, kmsvnc->drm->mfb->width, kmsvnc->drm->mfb->height); if (kmsvnc->capture_cursor) { cursor_frame++; cursor_frame %= CURSOR_FRAMESKIP; if (!cursor_frame) { char *data = NULL; int width, height; int err = drm_dump_cursor_plane(&data, &width, &height); if (!err && data) { update_vnc_cursor(data, width, height); } } } } } cleanup(); return 0; }