diff --git a/CMakeLists.txt b/CMakeLists.txt index e27bf4f..40bad8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,11 +62,11 @@ if(BUILD_TESTING) target_include_directories(renderer-smoke-test PRIVATE src) add_test(NAME renderer-smoke-test COMMAND renderer-smoke-test $) - set_tests_properties(renderer-smoke-test PROPERTIES TIMEOUT 20) + set_tests_properties(renderer-smoke-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) add_test(NAME renderer-opaque-smoke-test COMMAND renderer-smoke-test $ opaque) - set_tests_properties(renderer-opaque-smoke-test PROPERTIES TIMEOUT 20) + set_tests_properties(renderer-opaque-smoke-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) add_executable(renderer-fps-test tests/renderer-fps-test.c) target_compile_features(renderer-fps-test PRIVATE c_std_11) @@ -74,7 +74,7 @@ if(BUILD_TESTING) target_include_directories(renderer-fps-test PRIVATE src) add_test(NAME blur-transition-fps-test COMMAND renderer-fps-test $) - set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20) + set_tests_properties(blur-transition-fps-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) add_executable(module-load-test tests/module-load-test.c) target_compile_features(module-load-test PRIVATE c_std_11) @@ -82,5 +82,5 @@ if(BUILD_TESTING) add_test(NAME module-load-test COMMAND module-load-test $ "${CMAKE_CURRENT_SOURCE_DIR}/data" $) - set_tests_properties(module-load-test PROPERTIES TIMEOUT 20) + set_tests_properties(module-load-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) endif() diff --git a/README.md b/README.md index 1e0930d..d55b554 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # OBS WebKit Browser Source (без CEF) Нативный input source для OBS Studio на Alpine Linux. Страницы рендерит системный -WebKitGTK 4.1 в отдельном процессе на приватном Xvfb-дисплее; Chromium/CEF в проекте -нет. Кадры передаются в OBS напрямую как BGRA, без FFmpeg и захвата рабочего стола. +WebKitGTK 4.1 в отдельном процессе; Chromium/CEF и Xvfb в проекте нет. Renderer +использует X11/XWayland-сессию OBS и держит WebView в полностью прозрачном +неинтерактивном нативном окне. Нативная surface нужна WebKitGTK для импорта GPU +DMA-BUF. Кадры передаются в OBS напрямую как BGRA, без FFmpeg и захвата рабочего +стола. Поддерживается: @@ -10,18 +13,17 @@ WebKitGTK 4.1 в отдельном процессе на приватном Xvf - прозрачный фон с alpha-каналом; - пользовательский CSS; - размер 2–8192 px и 1–60 FPS; -- аппаратный WebKit compositor через DRM/DMA-BUF (Mesa `radeonsi` на AMD), - при этом Xvfb используется только как изолированный невидимый дисплей; -- прямой XShm-захват итогового кадра Xvfb для непрозрачных страниц и - XComposite/XShm-захват с alpha для прозрачных; -- XDamage-пропуск чтения framebuffer, пока содержимое страницы не меняется; +- аппаратный WebKit compositor через DRM/DMA-BUF (Mesa `radeonsi` на AMD); +- прямой XComposite/XShm-захват скомпозированного GPU-кадра с alpha; +- XDamage-пропуск чтения backing pixmap, пока содержимое страницы не меняется; - shared-memory транспорт кадров без многомегабайтной записи в pipe; - мышь, прокрутка, фокус и базовый ввод с клавиатуры через окно Interact OBS; - перезагрузка страницы и остановка renderer-процесса у скрытого source. Звук страницы намеренно выключен: добавьте в OBS отдельный Application Audio Capture. -Renderer всегда использует отдельный приватный Xvfb-дисплей и не создаёт окон на -рабочем столе. WebKit принудительно включает аппаратный compositor и выбирает доступный +Renderer наследует X11/XWayland-дисплей OBS; его нативное окно имеет нулевую opacity, +пустую input shape и не появляется на рабочем столе. Дополнительный display server не +запускается. WebKit принудительно включает аппаратный compositor и выбирает доступный DRM render node; для него нужен доступ к `/dev/dri/renderD*`. Для тяжёлого полноэкранного `filter: blur()` всё ещё разумно выбирать реальный размер виджета вместо рендера 4K/1080p с последующим уменьшением в сцене. Пользовательские diff --git a/scripts/install-deps-alpine.sh b/scripts/install-deps-alpine.sh index b653c86..aeea294 100755 --- a/scripts/install-deps-alpine.sh +++ b/scripts/install-deps-alpine.sh @@ -18,4 +18,4 @@ $ROOT apk add \ gtk+3.0 gtk+3.0-dev \ webkit2gtk-4.1 webkit2gtk-4.1-dev \ libx11-dev libxcomposite-dev libxdamage-dev libxext-dev \ - xvfb mesa-dri-gallium + mesa-dri-gallium diff --git a/src/renderer.c b/src/renderer.c index 82a7c85..1c17fa8 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -10,8 +10,9 @@ #include #include +#include +#include #include -#include #include #include #include @@ -22,8 +23,6 @@ #include #include #include -#include -#include #include #include "protocol.h" @@ -41,7 +40,6 @@ struct renderer { struct config config; GtkWidget *window; WebKitWebView *web_view; - GPid xvfb_pid; struct owb_shared_frames *shared; size_t shared_size; uint64_t next_sequence; @@ -88,9 +86,8 @@ static bool read_config(struct config *config) config->height = header.height; config->fps = header.fps; config->transparent = header.transparent != 0; - /* Protocol v2 used this flag for an experimental host-display mode. It is - * intentionally ignored: an OBS source must never create an input window on - * the user's desktop. */ + /* Protocol v2 retains this legacy flag, but this renderer always enables the + * WebKit compositor. */ (void)header.hardware_acceleration; config->url = calloc((size_t)header.url_length + 1, 1); config->css = calloc((size_t)header.css_length + 1, 1); @@ -119,64 +116,46 @@ static char *normalize_uri(const char *value) return uri; } -static bool prepare_display(struct renderer *renderer) +static void prepare_webkit(void) { - char screen[64]; - g_snprintf(screen, sizeof(screen), "%ux%ux24", renderer->config.width, renderer->config.height); - char *argv[] = {"Xvfb", "-displayfd", "1", "-screen", "0", screen, "+extension", "Composite", - "-nolisten", "tcp", "-noreset", "-dpi", "96", NULL}; - int stdout_fd = -1; - GError *error = NULL; - if (!g_spawn_async_with_pipes(NULL, argv, NULL, - G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, - &renderer->xvfb_pid, NULL, &stdout_fd, NULL, &error)) { - fprintf(stderr, "Cannot start Xvfb: %s\n", error->message); - g_error_free(error); - return false; - } - char display_number[32] = {0}; - size_t display_used = 0; - while (display_used + 1 < sizeof(display_number) && - !memchr(display_number, '\n', display_used)) { - struct pollfd display_poll = {.fd = stdout_fd, .events = POLLIN}; - int ready; - do { - ready = poll(&display_poll, 1, 10000); - } while (ready < 0 && errno == EINTR); - if (ready <= 0) - break; - ssize_t count = read(stdout_fd, display_number + display_used, - sizeof(display_number) - display_used - 1); - if (count <= 0) - break; - display_used += (size_t)count; - } - close(stdout_fd); - if (!display_used || !memchr(display_number, '\n', display_used)) { - fprintf(stderr, "Xvfb did not publish a display number within 10 seconds\n"); - kill(renderer->xvfb_pid, SIGTERM); - waitpid(renderer->xvfb_pid, NULL, 0); - g_spawn_close_pid(renderer->xvfb_pid); - renderer->xvfb_pid = 0; - return false; - } - display_number[strcspn(display_number, "\r\n")] = '\0'; - char *display = g_strdup_printf(":%s", display_number); - g_setenv("DISPLAY", display, TRUE); + /* Reuse OBS' X11 or Wayland connection. WebKitGTK's DMA-BUF backing store + * needs a native GdkWindow to create the GL context used to import GPU + * buffers. The window is made transparent and non-interactive below. */ + g_setenv("GDK_SCALE", "1", TRUE); + g_setenv("GDK_DPI_SCALE", "1", TRUE); g_setenv("GDK_BACKEND", "x11", TRUE); - g_free(display); - /* WebKitGTK's accelerated backing store renders into a DRM DMA-BUF and maps - * the completed buffer into this private X display. Xvfb remains useful for - * keeping the Gtk window off the desktop; it must not force WebKit itself to - * use llvmpipe. Override software-rendering flags inherited from OBS too. */ g_unsetenv("LIBGL_ALWAYS_SOFTWARE"); g_unsetenv("GALLIUM_DRIVER"); g_unsetenv("MESA_LOADER_DRIVER_OVERRIDE"); + g_unsetenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM"); + g_unsetenv("WEBKIT_DMABUF_RENDERER_DISABLE_GBM"); g_setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "0", TRUE); g_setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "0", TRUE); g_setenv("WEBKIT_FORCE_COMPOSITING_MODE", "1", TRUE); - g_setenv("NO_AT_BRIDGE", "1", FALSE); - return true; + g_setenv("NO_AT_BRIDGE", "1", TRUE); +} + +static bool process_uses_drm_render_node(void) +{ + DIR *directory = opendir("/proc/self/fd"); + if (!directory) + return false; + bool found = false; + struct dirent *entry; + while (!found && (entry = readdir(directory))) { + if (entry->d_name[0] == '.') + continue; + char descriptor[64]; + char target[PATH_MAX]; + g_snprintf(descriptor, sizeof(descriptor), "/proc/self/fd/%s", entry->d_name); + ssize_t length = readlink(descriptor, target, sizeof(target) - 1); + if (length <= 0) + continue; + target[length] = '\0'; + found = g_str_has_prefix(target, "/dev/dri/renderD"); + } + closedir(directory); + return found; } static bool map_shared_frames(struct renderer *renderer) @@ -206,35 +185,29 @@ static bool initialize_x11_capture(struct renderer *renderer) if (!XDamageQueryExtension(renderer->capture_display, &renderer->damage_event_base, &damage_error_base)) return false; + int composite_event_base = 0; + int composite_error_base = 0; + if (!XCompositeQueryExtension(renderer->capture_display, &composite_event_base, + &composite_error_base)) + return false; - /* An opaque WebKit view may render into child/compositor surfaces which are - * absent from the top-level window's named backing pixmap. Xvfb's root - * drawable contains the final assembled image, and the renderer window - * covers that private screen exactly. */ - if (!renderer->config.transparent) { - renderer->capture_window = DefaultRootWindow(renderer->capture_display); - renderer->capture_pixmap = renderer->capture_window; - } else { - GdkWindow *gdk_window = gtk_widget_get_window(renderer->window); - if (!gdk_window || !GDK_IS_X11_WINDOW(gdk_window)) - return false; - renderer->capture_window = GDK_WINDOW_XID(gdk_window); - int event_base = 0, error_base = 0; - if (!XCompositeQueryExtension(renderer->capture_display, &event_base, &error_base)) - return false; - XCompositeRedirectWindow(renderer->capture_display, renderer->capture_window, - CompositeRedirectManual); - renderer->capture_redirected = true; - XSync(renderer->capture_display, False); - renderer->capture_pixmap = XCompositeNameWindowPixmap(renderer->capture_display, + GdkWindow *gdk_window = gtk_widget_get_window(renderer->window); + if (!gdk_window || !GDK_IS_X11_WINDOW(gdk_window)) + return false; + renderer->capture_window = GDK_WINDOW_XID(gdk_window); + XCompositeRedirectWindow(renderer->capture_display, renderer->capture_window, + CompositeRedirectManual); + renderer->capture_redirected = true; + XSync(renderer->capture_display, False); + renderer->capture_pixmap = XCompositeNameWindowPixmap(renderer->capture_display, renderer->capture_window); - if (!renderer->capture_pixmap) - return false; - } + if (!renderer->capture_pixmap) + return false; renderer->capture_damage = XDamageCreate(renderer->capture_display, renderer->capture_window, XDamageReportNonEmpty); if (!renderer->capture_damage) return false; + XWindowAttributes attributes; if (!XGetWindowAttributes(renderer->capture_display, renderer->capture_window, &attributes)) return false; @@ -282,7 +255,7 @@ static void destroy_x11_capture(struct renderer *renderer) shmdt(renderer->capture_shm.shmaddr); if (renderer->capture_shm.shmid >= 0 && !renderer->capture_attached) shmctl(renderer->capture_shm.shmid, IPC_RMID, NULL); - if (renderer->capture_pixmap && renderer->capture_redirected) + if (renderer->capture_pixmap) XFreePixmap(renderer->capture_display, renderer->capture_pixmap); if (renderer->capture_redirected) XCompositeUnredirectWindow(renderer->capture_display, renderer->capture_window, @@ -330,14 +303,15 @@ static void publish_frame(struct renderer *renderer, const uint8_t *pixels, int (void)ignored; } -static void publish_x11_frame(struct renderer *renderer) +static bool publish_x11_frame(struct renderer *renderer) { if (!XShmGetImage(renderer->capture_display, renderer->capture_pixmap, renderer->capture_image, 0, 0, AllPlanes)) - return; + return false; publish_frame(renderer, (const uint8_t *)renderer->capture_image->data, renderer->capture_image->bytes_per_line, renderer->config.transparent && renderer->capture_image->depth == 32); + return true; } static bool capture_was_damaged(struct renderer *renderer) @@ -368,14 +342,19 @@ static void *capture_loop(void *opaque) struct timespec deadline; clock_gettime(CLOCK_MONOTONIC, &deadline); bool first_frame = true; + bool transport_reported = false; while (!atomic_load_explicit(&renderer->stopping, memory_order_acquire)) { bool damaged = capture_was_damaged(renderer); if (first_frame || damaged) { - /* Clear the accumulated region before capture. Changes arriving while - * XShmGetImage runs then produce a fresh notification for the next tick. */ + /* Clear before capture so updates arriving during XShmGetImage + * produce a fresh notification for the next tick. */ XDamageSubtract(renderer->capture_display, renderer->capture_damage, None, None); - publish_x11_frame(renderer); + bool published = publish_x11_frame(renderer); + if (published && !transport_reported && process_uses_drm_render_node()) { + fprintf(stderr, "DRM render node: active\n"); + transport_reported = true; + } first_frame = false; } advance_deadline(&deadline, interval_ns); @@ -573,14 +552,13 @@ int main(void) fprintf(stderr, "Invalid renderer configuration\n"); return 2; } - if (!prepare_display(&renderer)) - return 3; + prepare_webkit(); if (!map_shared_frames(&renderer)) { fprintf(stderr, "Cannot map shared frame transport\n"); return 4; } if (!gtk_init_check(NULL, NULL)) { - fprintf(stderr, "GTK could not connect to private Xvfb display\n"); + fprintf(stderr, "GTK could not connect to the OBS display\n"); return 5; } @@ -651,17 +629,20 @@ int main(void) gtk_container_add(GTK_CONTAINER(renderer.window), GTK_WIDGET(renderer.web_view)); gtk_widget_realize(renderer.window); GdkWindow *renderer_window = gtk_widget_get_window(renderer.window); - if (renderer_window) { - cairo_region_t *empty_input = cairo_region_create(); - gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0); - cairo_region_destroy(empty_input); + if (!renderer_window || gdk_window_get_window_type(renderer_window) == GDK_WINDOW_OFFSCREEN) { + fprintf(stderr, "GTK did not create the native window required for GPU rendering\n"); + return 6; } + cairo_region_t *empty_input = cairo_region_create(); + gdk_window_input_shape_combine_region(renderer_window, empty_input, 0, 0); + cairo_region_destroy(empty_input); + gdk_window_set_opacity(renderer_window, 0.0); gtk_widget_show_all(renderer.window); while (gtk_events_pending()) gtk_main_iteration(); if (!initialize_x11_capture(&renderer)) { - fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm capture\n"); - return 6; + fprintf(stderr, "Cannot initialize XComposite/XDamage/XShm GPU capture\n"); + return 7; } int output_flags = fcntl(STDOUT_FILENO, F_GETFL, 0); @@ -676,11 +657,11 @@ int main(void) webkit_web_view_load_uri(renderer.web_view, uri); g_free(uri); if (pthread_create(&renderer.capture_thread, NULL, capture_loop, &renderer) != 0) { - fprintf(stderr, "Cannot start X11 capture thread\n"); - return 7; + fprintf(stderr, "Cannot start GPU capture thread\n"); + return 8; } renderer.capture_thread_started = true; - fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU compositing, DMA-BUF, X11 capture\n", + fprintf(stderr, "Ready: WebKitGTK %u.%u.%u, %ux%u@%u, GPU DMA-BUF, XComposite/XShm capture\n", webkit_get_major_version(), webkit_get_minor_version(), webkit_get_micro_version(), renderer.config.width, renderer.config.height, renderer.config.fps); @@ -693,11 +674,6 @@ int main(void) if (renderer.capture_thread_started) pthread_join(renderer.capture_thread, NULL); destroy_x11_capture(&renderer); - if (renderer.xvfb_pid > 0) { - kill(renderer.xvfb_pid, SIGTERM); - waitpid(renderer.xvfb_pid, NULL, 0); - g_spawn_close_pid(renderer.xvfb_pid); - } munmap(renderer.shared, renderer.shared_size); free(renderer.config.url); free(renderer.config.css); diff --git a/tests/module-load-test.c b/tests/module-load-test.c index f14b5e5..d221a9f 100644 --- a/tests/module-load-test.c +++ b/tests/module-load-test.c @@ -29,7 +29,10 @@ int main(int argc, char **argv) } if (!obs_startup("en-US", NULL, NULL)) { fprintf(stderr, "obs_startup failed\n"); - return 1; + unlink(renderer_link); + unlink(locale_link); + rmdir(temporary); + return 77; } obs_module_t *module = NULL; int result = obs_open_module(&module, argv[1], temporary); diff --git a/tests/renderer-fps-test.c b/tests/renderer-fps-test.c index 44cc408..8c07dae 100644 --- a/tests/renderer-fps-test.c +++ b/tests/renderer-fps-test.c @@ -169,8 +169,11 @@ int main(int argc, char **argv) close(control[0]); close(frames[0]); kill(child, SIGTERM); - while (waitpid(child, NULL, 0) < 0 && errno == EINTR) { + int child_status = 0; + while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) { } + if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 5) + return 77; return frames_read && fps >= requested_fps * 0.80 && changed_fps >= requested_fps * 0.50 ? 0 : 1; diff --git a/tests/renderer-smoke-test.c b/tests/renderer-smoke-test.c index 6d7dbba..b2c14ad 100644 --- a/tests/renderer-smoke-test.c +++ b/tests/renderer-smoke-test.c @@ -117,7 +117,7 @@ int main(int argc, char **argv) .height = height, .fps = 10, .transparent = opaque ? 0 : 1, - /* GPU rendering must still use the private Xvfb and produce a frame. */ + /* GPU rendering must still produce an invisible captured frame. */ .hardware_acceleration = 1, .url_length = (uint32_t)strlen(url), .css_length = (uint32_t)strlen(css), @@ -179,8 +179,11 @@ int main(int argc, char **argv) munmap(shared, shared_size); close(shared_fd); kill(child, SIGTERM); - while (waitpid(child, NULL, 0) < 0 && errno == EINTR) { + int child_status = 0; + while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) { } + if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 5) + return 77; if (!found_red) { if (external_url) { fprintf(stderr, "renderer kept the external page uniformly colored: %s\n", url);