optimization

This commit is contained in:
2026-07-18 10:06:53 +05:00
parent 7d91df3c6b
commit 2c166a40e3
7 changed files with 108 additions and 121 deletions

View File

@@ -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 $<TARGET_FILE:obs-webkit-renderer>)
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 $<TARGET_FILE:obs-webkit-renderer> 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 $<TARGET_FILE:obs-webkit-renderer>)
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
$<TARGET_FILE:obs-webkit-browser> "${CMAKE_CURRENT_SOURCE_DIR}/data"
$<TARGET_FILE:obs-webkit-renderer>)
set_tests_properties(module-load-test PROPERTIES TIMEOUT 20)
set_tests_properties(module-load-test PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77)
endif()

View File

@@ -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;
- размер 28192 px и 160 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 с последующим уменьшением в сцене. Пользовательские

View File

@@ -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

View File

@@ -10,8 +10,9 @@
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <limits.h>
#include <pthread.h>
#include <poll.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
@@ -22,8 +23,6 @@
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#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,22 +185,16 @@ 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;
@@ -230,11 +203,11 @@ static bool initialize_x11_capture(struct renderer *renderer)
renderer->capture_window);
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) {
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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);