diff --git a/mk/osiris.mk b/mk/osiris.mk index 79ccf1b..a525ac2 100644 --- a/mk/osiris.mk +++ b/mk/osiris.mk @@ -156,7 +156,7 @@ OSIRIS_DIFF_JOBS ?= 1 OSIRIS_DIFF_INSTALL_DEPS ?= 0 OSIRIS_DIFF_LOCAL ?= 0 OSIRIS_DIFF_MAE_THRESHOLD ?= 0.16 -OSIRIS_DIFF_CHANGED_THRESHOLD ?= 0.46 +OSIRIS_DIFF_CHANGED_THRESHOLD ?= 0.55 OSIRIS_DIFF_GEOMETRY ?= 1280x1024x24 OSIRIS_DIFF_TOP ?= 12 OSIRIS_DIFF_COMPARE_LOCATION ?= $(if $(filter 1 yes true,$(OSIRIS_DIFF_LOCAL)),local,remote) diff --git a/scripts/run-osiris-differential-tests.py b/scripts/run-osiris-differential-tests.py index 9bc56b1..c38db88 100755 --- a/scripts/run-osiris-differential-tests.py +++ b/scripts/run-osiris-differential-tests.py @@ -216,7 +216,13 @@ def remote_script(args, remote_repo): input_backend=$9 replay_out="$remote_root/replay-$name" rm -rf "$replay_out" "$remote_root/home-$name" - mkdir -p "$log_dir" "$screen_dir" "$remote_root/home-$name" + mkdir -p "$replay_out" "$log_dir" "$screen_dir" "$remote_root/home-$name" + replay_path="$repo/tests/ui/replays/$replay" + if [ "$input_backend" = xdotool ]; then + replay_path="$replay_out/$replay" + awk '/^wait-window / {{ sub(/[0-9]+[ \t]*$/, "15000") }} {{ print }}' \ + "$repo/tests/ui/replays/$replay" > "$replay_path" + fi # Read display from the current env so the parallel capture # subshells can each target their own Xvfb. Strip the leading # colon and any trailing .screen suffix to recover the numeric @@ -228,12 +234,13 @@ def remote_script(args, remote_repo): --app "$app" \\ --app-arg=-geometry --app-arg="$geometry_arg" \\ --workdir "$workdir" \\ - --replay "$repo/tests/ui/replays/$replay" \\ + --replay "$replay_path" \\ --out-root "$replay_out" \\ --display "$display_num" \\ --geometry {q(args.geometry)} \\ --input-backend "$input_backend" \\ --screenshot-command import \\ + $([ "$input_backend" = internal ] && printf %s --in-process-snapshots) \\ --screenshot-region {q(args.screenshot_region)} \\ --env DISPLAY="$DISPLAY" \\ --env HOME="$remote_root/home-$name" \\ @@ -362,10 +369,34 @@ def remote_script(args, remote_repo): xvfb_pid=$! Xvfb "$compat_display" -screen 0 {q(args.geometry)} >"$remote_root/xvfb-compat.log" 2>&1 & compat_xvfb_pid=$! +trap 'exit' INT TERM HUP trap 'kill "$xvfb_pid" "$compat_xvfb_pid" >/dev/null 2>&1 || true' EXIT -sleep 1 -# Osiris has 4 demos per side and each capture runs a replay so the +wait_for_display() {{ + target=$1 + server_pid=$2 + waited=0 + while [ "$waited" -lt 100 ]; do + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "Xvfb for $target exited before accepting connections" >&2 + return 1 + fi + if DISPLAY="$target" xdotool getdisplaygeometry >/dev/null 2>&1; then + return 0 + fi + # ponytail: sub-second poll assumes GNU coreutils sleep (the CI + # runner); switch to integer sleep if a POSIX-only sleep is ever + # targeted. + sleep 0.1 + waited=$((waited + 1)) + done + echo "Xvfb for $target did not become ready within 10s" >&2 + return 1 +}} +wait_for_display "$display" "$xvfb_pid" +wait_for_display "$compat_display" "$compat_xvfb_pid" + +# Osiris has 5 demos per side and each capture runs a replay so the # capture phase dominates the job. Run system-side and compat-side # captures concurrently on separate Xvfb instances. system_cap_log="$remote_root/logs/system-capture.log" @@ -412,6 +443,15 @@ def remote_script(args, remote_repo): "$system_logs" \\ "$system_screens" \\ xdotool + capture_osiris system-designer-menu \\ + "$system_designer" \\ + "$osiris_src/tools/designer/designer" \\ + osiris-designer-menu.replay \\ + 940x740+0+0 \\ + "$system_build" \\ + "$system_logs" \\ + "$system_screens" \\ + xdotool ) >"$system_cap_log" 2>&1 & system_cap_pid=$! @@ -454,6 +494,15 @@ def remote_script(args, remote_repo): "$compat_logs" \\ "$compat_screens" \\ internal + capture_osiris compat-designer-menu \\ + "$compat_designer" \\ + "$osiris_src/tools/designer/designer" \\ + osiris-designer-menu.replay \\ + 940x740+0+0 \\ + "$repo/build/osiris/build:$repo/build" \\ + "$compat_logs" \\ + "$compat_screens" \\ + internal ) >"$compat_cap_log" 2>&1 & compat_cap_pid=$! diff --git a/src/snapshot.c b/src/snapshot.c index c0a36a4..5cd3aa1 100644 --- a/src/snapshot.c +++ b/src/snapshot.c @@ -28,6 +28,7 @@ #include "events.h" #include "replay-target.h" #include "snapshot.h" +#include "window.h" #include "window-internal.h" #include "util.h" @@ -181,6 +182,68 @@ static int waitSnapshotResult(int *waitRcOut) return rc; } +/* In-process snapshots read a single SDL window surface (the replay + * target). Qt popup menus and other override-redirect shells live in + * their own borderless SDL windows, so they are absent from that read + * even though they render correctly. Composite every mapped + * override-redirect top-level onto a copy of the target surface, in + * stacking order, so the snapshot matches what a whole-screen capture + * (the system side's import) would see. + * + * Returns a newly allocated surface the caller must free, or NULL when + * no popup is present, in which case the caller saves the target + * surface unchanged. + */ +static SDL_Surface *composeOverlayPopups(SDL_Surface *target, Window targetWin) +{ + if (SCREEN_WINDOW == None || targetWin == None) + return NULL; + WindowStruct *targetStruct = GET_WINDOW_STRUCT(targetWin); + Window *children = GET_CHILDREN(SCREEN_WINDOW); + size_t count = GET_WINDOW_STRUCT(SCREEN_WINDOW)->children.length; + SDL_Surface *composed = NULL; + for (size_t i = 0; i < count; i++) { + Window child = children[i]; + if (child == targetWin) + continue; + WindowStruct *childStruct = GET_WINDOW_STRUCT(child); + if (!childStruct->overrideRedirect || childStruct->mapState != Mapped || + !childStruct->sdlWindow) + continue; + SDL_Surface *childSurface = + SDL_GetWindowSurface(childStruct->sdlWindow); + if (!childSurface) + continue; + if (!composed) { + /* SDL_DuplicateSurface is absent from the older SDL2 on the + * differential runners, so build the copy the way drawing.c + * does: a matching-format surface plus a base blit. + */ + composed = SDL_CreateRGBSurfaceWithFormat( + 0, target->w, target->h, 32, XC_SURFACE_FMT_ENUM(target)); + if (!composed) + return NULL; + if (SDL_BlitSurface(target, NULL, composed, NULL) != 0) { + SDL_FreeSurface(composed); + return NULL; + } + } + /* ponytail: 1:1 X-logical to surface pixel mapping, correct on the + * Xvfb CI path. A HiDPI host would need the surface-to-logical + * scale folded into the offset; blit clipping handles popups that + * fall partly or wholly outside the target rect. + */ + SDL_Rect dst = { + .x = childStruct->x - targetStruct->x, + .y = childStruct->y - targetStruct->y, + .w = childSurface->w, + .h = childSurface->h, + }; + SDL_BlitSurface(childSurface, NULL, composed, &dst); + } + return composed; +} + int snapshotHandleEvent(const SDL_Event *event) { SnapshotEnvelope *env = (SnapshotEnvelope *) event->user.data1; @@ -195,6 +258,7 @@ int snapshotHandleEvent(const SDL_Event *event) char *path = env->path; int rc = 0; SDL_Surface *ownedSurface = NULL; + SDL_Surface *composed = NULL; Uint32 winId = replayTargetWindowId(); SDL_Window *win = (winId != 0) ? SDL_GetWindowFromID(winId) : NULL; if (!win) { @@ -228,6 +292,12 @@ int snapshotHandleEvent(const SDL_Event *event) } } + /* Fold any mapped popup windows (Qt menus etc.) into the saved image so + * the single-window read matches the system side's whole-screen capture. + */ + composed = composeOverlayPopups(surface, getWindowFromId(winId)); + SDL_Surface *saveSurface = composed ? composed : surface; + /* SDL_SaveBMP writes incrementally to the open file, so a runner that polls * for this path can observe a non-empty but truncated BMP and fail to * decode it (PIL "image file is truncated"). Write to a temp file and @@ -248,7 +318,7 @@ int snapshotHandleEvent(const SDL_Event *event) } memcpy(tmpPath, path, pathLen); memcpy(tmpPath + pathLen, ".tmp", sizeof(".tmp")); - if (SDL_SaveBMP(surface, tmpPath) != 0) { + if (SDL_SaveBMP(saveSurface, tmpPath) != 0) { LOG("snapshot: SDL_SaveBMP(%s) failed: %s\n", tmpPath, SDL_GetError()); free(tmpPath); rc = -3; @@ -262,8 +332,10 @@ int snapshotHandleEvent(const SDL_Event *event) goto signal; } free(tmpPath); - LOG("snapshot: wrote %s (%dx%d)\n", path, surface->w, surface->h); + LOG("snapshot: wrote %s (%dx%d)\n", path, saveSurface->w, saveSurface->h); signal: + if (composed) + SDL_FreeSurface(composed); if (ownedSurface) SDL_FreeSurface(ownedSurface); signalSnapshotResult(env->generation, rc);