mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 02:00:06 +02:00
Add a headless test mode to ft-screens and a throwaway test desktop
ft-screens --no-vr runs without SteamVR and leaves the input relay alone; --control names its control socket; "toplevels" lists KWin's windows with their titles and sizes; "input" feeds pointer events as if from a panel. screens/test/headless.sh starts it with a bare nested KWin next to the running desktop, and loads KWin scripts, runs apps, and takes screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a88a6b7c9
commit
42a755b9f4
4 files changed
+272
-12
No files matched your search
+77
-11
@@ -12,8 +12,13 @@
|
|||||||
// buffers are in pixels, so a panel position in pixels is divided by the screen's KWin
|
// buffers are in pixels, so a panel position in pixels is divided by the screen's KWin
|
||||||
// scale first ("scale <screen> <s>", from ft-layout).
|
// scale first ("scale <screen> <s>", from ft-layout).
|
||||||
//
|
//
|
||||||
// Usage: ft-screens [--socket NAME] [--screen WxH@METRES]... [-- COMMAND ARGS...]
|
// Usage: ft-screens [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]...
|
||||||
|
// [-- COMMAND ARGS...]
|
||||||
// --socket Wayland socket name in $XDG_RUNTIME_DIR (default ft-screens-0)
|
// --socket Wayland socket name in $XDG_RUNTIME_DIR (default ft-screens-0)
|
||||||
|
// --control the control socket's abstract name (default ft_screens)
|
||||||
|
// --no-vr run without SteamVR, for tests next to the running desktop: no panels, no
|
||||||
|
// input, and nothing sent to the input relay. Commands still work, and
|
||||||
|
// "toplevels" shows what KWin opened.
|
||||||
// --screen one per screen, in KWin's order (default: 3440x1440@2.4)
|
// --screen one per screen, in KWin's order (default: 3440x1440@2.4)
|
||||||
// COMMAND run with WAYLAND_DISPLAY set to our socket (e.g. the Frametop session)
|
// COMMAND run with WAYLAND_DISPLAY set to our socket (e.g. the Frametop session)
|
||||||
// Runs in the dev container (wlroots 0.20); KWin connects from the host.
|
// Runs in the dev container (wlroots 0.20); KWin connects from the host.
|
||||||
@@ -68,8 +73,10 @@ struct screen {
|
|||||||
struct wlr_xdg_toplevel *toplevel;
|
struct wlr_xdg_toplevel *toplevel;
|
||||||
struct wlr_buffer *held; // on the panel now; unlocked when the next one arrives
|
struct wlr_buffer *held; // on the panel now; unlocked when the next one arrives
|
||||||
bool frame_pending; // a commit waits for its frame callback
|
bool frame_pending; // a commit waits for its frame callback
|
||||||
|
unsigned commits; // buffers committed, and the last one's size ("toplevels")
|
||||||
|
int buffer_width, buffer_height;
|
||||||
struct wlr_xdg_toplevel_decoration_v1 *decoration; // answered on the first commit
|
struct wlr_xdg_toplevel_decoration_v1 *decoration; // answered on the first commit
|
||||||
struct wl_listener commit, destroy, decoration_destroy;
|
struct wl_listener commit, destroy, decoration_destroy, set_title;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Per client buffer: forget its import when it goes away.
|
// Per client buffer: forget its import when it goes away.
|
||||||
@@ -102,6 +109,7 @@ struct server {
|
|||||||
int relay_fd; // unbound, so the relay can't reply into our control socket
|
int relay_fd; // unbound, so the relay can't reply into our control socket
|
||||||
uint32_t relay_sent; // when the relay last heard from us (ms)
|
uint32_t relay_sent; // when the relay last heard from us (ms)
|
||||||
unsigned ticks;
|
unsigned ticks;
|
||||||
|
bool vr; // connected to SteamVR (not --no-vr)
|
||||||
};
|
};
|
||||||
|
|
||||||
static uint32_t now_ms(void) {
|
static uint32_t now_ms(void) {
|
||||||
@@ -153,6 +161,8 @@ static void screen_commit(struct wl_listener *l, void *data) {
|
|||||||
}
|
}
|
||||||
if (!buffer) return;
|
if (!buffer) return;
|
||||||
sc->frame_pending = true;
|
sc->frame_pending = true;
|
||||||
|
++sc->commits;
|
||||||
|
sc->buffer_width = buffer->width, sc->buffer_height = buffer->height;
|
||||||
if (buffer == sc->held) return;
|
if (buffer == sc->held) return;
|
||||||
struct wlr_dmabuf_attributes a;
|
struct wlr_dmabuf_attributes a;
|
||||||
if (!wlr_buffer_get_dmabuf(buffer, &a)) {
|
if (!wlr_buffer_get_dmabuf(buffer, &a)) {
|
||||||
@@ -187,9 +197,17 @@ static void screen_destroy(struct wl_listener *l, void *data) {
|
|||||||
sc->server->screens[sc->index] = NULL;
|
sc->server->screens[sc->index] = NULL;
|
||||||
wl_list_remove(&sc->commit.link);
|
wl_list_remove(&sc->commit.link);
|
||||||
wl_list_remove(&sc->destroy.link);
|
wl_list_remove(&sc->destroy.link);
|
||||||
|
wl_list_remove(&sc->set_title.link);
|
||||||
free(sc);
|
free(sc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KWin titles each window "KDE Wayland Compositor <output name>", with "- Output disabled"
|
||||||
|
// after it while that output is off.
|
||||||
|
static void screen_title(struct wl_listener *l, void *data) {
|
||||||
|
struct screen *sc = wl_container_of(l, sc, set_title);
|
||||||
|
wlr_log(WLR_INFO, "screen %d: \"%s\"", sc->index + 1, sc->toplevel->title ? sc->toplevel->title : "");
|
||||||
|
}
|
||||||
|
|
||||||
static void new_toplevel(struct wl_listener *l, void *data) {
|
static void new_toplevel(struct wl_listener *l, void *data) {
|
||||||
struct server *s = wl_container_of(l, s, new_toplevel);
|
struct server *s = wl_container_of(l, s, new_toplevel);
|
||||||
struct wlr_xdg_toplevel *toplevel = data;
|
struct wlr_xdg_toplevel *toplevel = data;
|
||||||
@@ -211,6 +229,8 @@ static void new_toplevel(struct wl_listener *l, void *data) {
|
|||||||
wl_signal_add(&toplevel->base->surface->events.commit, &sc->commit);
|
wl_signal_add(&toplevel->base->surface->events.commit, &sc->commit);
|
||||||
sc->destroy.notify = screen_destroy;
|
sc->destroy.notify = screen_destroy;
|
||||||
wl_signal_add(&toplevel->events.destroy, &sc->destroy);
|
wl_signal_add(&toplevel->events.destroy, &sc->destroy);
|
||||||
|
sc->set_title.notify = screen_title;
|
||||||
|
wl_signal_add(&toplevel->events.set_title, &sc->set_title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// KWin asks for server-side decorations for its screens; we draw none. It asks before its
|
// KWin asks for server-side decorations for its screens; we draw none. It asks before its
|
||||||
@@ -294,6 +314,7 @@ static void handle_vr_event(const struct ft_event *e, void *data) {
|
|||||||
// gamescope's focus) doesn't get the keys too. Without word from us for a few seconds,
|
// gamescope's focus) doesn't get the keys too. Without word from us for a few seconds,
|
||||||
// the relay gives the keyboards back, so a closed desktop doesn't keep them.
|
// the relay gives the keyboards back, so a closed desktop doesn't keep them.
|
||||||
static void keys_update(struct server *s) {
|
static void keys_update(struct server *s) {
|
||||||
|
if (!s->vr) return; // a test instance leaves the running desktop's keyboards alone
|
||||||
const bool desktop = s->keys_clicked && ft_vr_screens_shown();
|
const bool desktop = s->keys_clicked && ft_vr_screens_shown();
|
||||||
const uint32_t t = now_ms();
|
const uint32_t t = now_ms();
|
||||||
if (desktop == s->keys_desktop && t - s->relay_sent < 1000) return;
|
if (desktop == s->keys_desktop && t - s->relay_sent < 1000) return;
|
||||||
@@ -366,6 +387,9 @@ static void handle_key(struct server *s, uint32_t code, int value, char *reply,
|
|||||||
|
|
||||||
// Control socket: abstract datagram @ft_screens. Here: "size <screen> <w> <h>" (a new
|
// Control socket: abstract datagram @ft_screens. Here: "size <screen> <w> <h>" (a new
|
||||||
// resolution, live), "scale <screen> <s>" (KWin's scale for it, from ft-layout),
|
// resolution, live), "scale <screen> <s>" (KWin's scale for it, from ft-layout),
|
||||||
|
// "toplevels" (-> "ok <count>" and a line per KWin window: "<screen> <w>x<h> <commits>
|
||||||
|
// <title>"), "input <screen> move|down|up|leave [x y [left|right|middle]]" (pointer input
|
||||||
|
// as if from that panel, x and y in its pixels; for tests, mostly with --no-vr),
|
||||||
// "key <code> <value>", and "click <overlay key>" (from the pointer
|
// "key <code> <value>", and "click <overlay key>" (from the pointer
|
||||||
// helper: a mouse click landed on that panel, "-" for none); the rest is in vr.cpp
|
// helper: a mouse click landed on that panel, "-" for none); the rest is in vr.cpp
|
||||||
// (ft_vr_command).
|
// (ft_vr_command).
|
||||||
@@ -402,6 +426,34 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
|||||||
handle_key(s, code, value, reply, sizeof reply);
|
handle_key(s, code, value, reply, sizeof reply);
|
||||||
len = sizeof from;
|
len = sizeof from;
|
||||||
continue; // no reply: keys are fire-and-forget
|
continue; // no reply: keys are fire-and-forget
|
||||||
|
} else if (strncmp(buf, "input ", 6) == 0) {
|
||||||
|
char what[8] = "", button[8] = "left";
|
||||||
|
double x = 0, y = 0;
|
||||||
|
const int got = sscanf(buf, "input %d %7s %lf %lf %7s", &index, what, &x, &y, button);
|
||||||
|
struct ft_event e = {.screen = index - 1, .x = x, .y = y, .button = BTN_LEFT};
|
||||||
|
if (strcmp(button, "right") == 0) e.button = BTN_RIGHT;
|
||||||
|
else if (strcmp(button, "middle") == 0) e.button = BTN_MIDDLE;
|
||||||
|
if (got >= 2 && strcmp(what, "leave") == 0) e.type = FT_LEAVE;
|
||||||
|
else if (got >= 4 && strcmp(what, "move") == 0) e.type = FT_MOTION;
|
||||||
|
else if (got >= 4 && (strcmp(what, "down") == 0 || strcmp(what, "up") == 0))
|
||||||
|
e.type = FT_BUTTON, e.pressed = what[0] == 'd';
|
||||||
|
else index = 0;
|
||||||
|
if (index < 1 || index > MAX_SCREENS || !s->screens[index - 1]) {
|
||||||
|
snprintf(reply, sizeof reply, "error input <screen> move|down|up|leave [x y [button]]");
|
||||||
|
} else {
|
||||||
|
handle_vr_event(&e, s);
|
||||||
|
snprintf(reply, sizeof reply, "ok");
|
||||||
|
}
|
||||||
|
} else if (strcmp(buf, "toplevels") == 0) {
|
||||||
|
int n = 0, at = 0;
|
||||||
|
for (int i = 0; i < MAX_SCREENS; ++i) n += s->screens[i] != NULL;
|
||||||
|
at = snprintf(reply, sizeof reply, "ok %d", n);
|
||||||
|
for (int i = 0; i < MAX_SCREENS && at < (int)sizeof reply; ++i) {
|
||||||
|
const struct screen *sc = s->screens[i];
|
||||||
|
if (!sc) continue;
|
||||||
|
at += snprintf(reply + at, sizeof reply - at, "\n%d %dx%d %u %s", i + 1, sc->buffer_width,
|
||||||
|
sc->buffer_height, sc->commits, sc->toplevel->title ? sc->toplevel->title : "");
|
||||||
|
}
|
||||||
} else if (strncmp(buf, "click ", 6) == 0) {
|
} else if (strncmp(buf, "click ", 6) == 0) {
|
||||||
// A click on another panel takes typing to Steam. Our own panels (the screens,
|
// A click on another panel takes typing to Steam. Our own panels (the screens,
|
||||||
// their controls) leave it: a click on a screen arrives as a panel event.
|
// their controls) leave it: a click on a screen arrives as a panel event.
|
||||||
@@ -418,13 +470,18 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int open_control_socket(void) {
|
static int open_control_socket(const char *name) {
|
||||||
int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||||
struct sockaddr_un addr = {.sun_family = AF_UNIX};
|
struct sockaddr_un addr = {.sun_family = AF_UNIX};
|
||||||
const char name[] = "ft_screens";
|
const size_t len = strlen(name);
|
||||||
memcpy(addr.sun_path + 1, name, sizeof name - 1);
|
if (len == 0 || len >= sizeof addr.sun_path - 1) {
|
||||||
if (bind(fd, (struct sockaddr *)&addr, offsetof(struct sockaddr_un, sun_path) + 1 + sizeof name - 1) != 0) {
|
wlr_log(WLR_ERROR, "bad control socket name");
|
||||||
wlr_log(WLR_ERROR, "can't bind @ft_screens (another ft-screens running?)");
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(addr.sun_path + 1, name, len);
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, offsetof(struct sockaddr_un, sun_path) + 1 + len) != 0) {
|
||||||
|
wlr_log(WLR_ERROR, "can't bind @%s (another ft-screens running?)", name);
|
||||||
close(fd);
|
close(fd);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -467,11 +524,16 @@ static bool setup_dmabuf(struct server *s) {
|
|||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
struct server s = {0};
|
struct server s = {0};
|
||||||
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
|
for (int i = 0; i < MAX_SCREENS; ++i) s.scale[i] = 1;
|
||||||
const char *socket_name = "ft-screens-0";
|
const char *socket_name = "ft-screens-0", *control_name = "ft_screens";
|
||||||
char **command = NULL;
|
char **command = NULL;
|
||||||
|
s.vr = true;
|
||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
if (strcmp(argv[i], "--socket") == 0 && i + 1 < argc) {
|
if (strcmp(argv[i], "--socket") == 0 && i + 1 < argc) {
|
||||||
socket_name = argv[++i];
|
socket_name = argv[++i];
|
||||||
|
} else if (strcmp(argv[i], "--control") == 0 && i + 1 < argc) {
|
||||||
|
control_name = argv[++i];
|
||||||
|
} else if (strcmp(argv[i], "--no-vr") == 0) {
|
||||||
|
s.vr = false;
|
||||||
} else if (strcmp(argv[i], "--screen") == 0 && i + 1 < argc && s.n_config < MAX_SCREENS) {
|
} else if (strcmp(argv[i], "--screen") == 0 && i + 1 < argc && s.n_config < MAX_SCREENS) {
|
||||||
struct config *c = &s.config[s.n_config];
|
struct config *c = &s.config[s.n_config];
|
||||||
c->metres = 0;
|
c->metres = 0;
|
||||||
@@ -485,7 +547,10 @@ int main(int argc, char **argv) {
|
|||||||
command = &argv[i + 1];
|
command = &argv[i + 1];
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "usage: %s [--socket NAME] [--screen WxH@METRES]... [-- COMMAND ARGS...]\n", argv[0]);
|
fprintf(stderr,
|
||||||
|
"usage: %s [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]... "
|
||||||
|
"[-- COMMAND ARGS...]\n",
|
||||||
|
argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,7 +558,8 @@ int main(int argc, char **argv) {
|
|||||||
|
|
||||||
setvbuf(stdout, NULL, _IOLBF, 0); // vr.cpp prints to stdout; keep it in order with the log
|
setvbuf(stdout, NULL, _IOLBF, 0); // vr.cpp prints to stdout; keep it in order with the log
|
||||||
wlr_log_init(WLR_INFO, NULL);
|
wlr_log_init(WLR_INFO, NULL);
|
||||||
if (!ft_vr_init()) return 1;
|
if (s.vr && !ft_vr_init()) return 1;
|
||||||
|
if (!s.vr) wlr_log(WLR_INFO, "--no-vr: running without SteamVR");
|
||||||
|
|
||||||
s.display = wl_display_create();
|
s.display = wl_display_create();
|
||||||
s.loop = wl_display_get_event_loop(s.display);
|
s.loop = wl_display_get_event_loop(s.display);
|
||||||
@@ -530,7 +596,7 @@ int main(int argc, char **argv) {
|
|||||||
snprintf(path, sizeof path, "%s/%s", getenv("XDG_RUNTIME_DIR") ? getenv("XDG_RUNTIME_DIR") : "/tmp", socket_name);
|
snprintf(path, sizeof path, "%s/%s", getenv("XDG_RUNTIME_DIR") ? getenv("XDG_RUNTIME_DIR") : "/tmp", socket_name);
|
||||||
wlr_log(WLR_INFO, "listening on %s; %d screen(s) configured", path, s.n_config);
|
wlr_log(WLR_INFO, "listening on %s; %d screen(s) configured", path, s.n_config);
|
||||||
|
|
||||||
const int control = open_control_socket();
|
const int control = open_control_socket(control_name);
|
||||||
if (control < 0) return 1;
|
if (control < 0) return 1;
|
||||||
s.relay_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
s.relay_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||||
wl_event_loop_add_fd(s.loop, control, WL_EVENT_READABLE, control_readable, &s);
|
wl_event_loop_add_fd(s.loop, control, WL_EVENT_READABLE, control_readable, &s);
|
||||||
|
|||||||
Executable
+160
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# A throwaway desktop for experiments, next to the running one: ft-screens --no-vr (its own
|
||||||
|
# Wayland socket, control socket @ft_screens_test, and runtime folder) with a bare nested
|
||||||
|
# KWin inside it, on its own D-Bus and config folder. Nothing touches SteamVR, the input
|
||||||
|
# relay, or the real desktop's settings. Run it from the PC (it runs itself on the Frame)
|
||||||
|
# or on the Frame. Build ft-screens first (screens/build.sh).
|
||||||
|
#
|
||||||
|
# headless.sh start [SCREENS] [SPARES] (default 2 screens, 1920x1080 and 1080x1920, and
|
||||||
|
# 3 spare 800x600 outputs, disabled once KWin is up)
|
||||||
|
# headless.sh stop
|
||||||
|
# headless.sh ask '<ft-screens command>' e.g. toplevels, "input 1 down 400 20"
|
||||||
|
# headless.sh kd <kscreen-doctor args> e.g. -o, output.WL-2.enable
|
||||||
|
# headless.sh run <command> [args] start an app in the test KWin (in the background)
|
||||||
|
# headless.sh script <file.js> [name] load a KWin script into it and run it
|
||||||
|
# headless.sh unscript <name>
|
||||||
|
# headless.sh js '<javascript>' run a one-off KWin script; its print()s come back
|
||||||
|
# headless.sh shot [OUTPUT] a JPEG of one output (default WL-0); from the PC it
|
||||||
|
# lands in captures/ and its path is printed
|
||||||
|
# headless.sh log [kwin|screens|apps] [N] the last N lines (default 30)
|
||||||
|
set -euo pipefail
|
||||||
|
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
. "$here/../../scripts/_env.sh"
|
||||||
|
|
||||||
|
if [ "$FRAME_LOCAL" != 1 ]; then
|
||||||
|
"$REPO_ROOT/scripts/sync.sh" >/dev/null
|
||||||
|
if [ "${1:-}" = shot ]; then
|
||||||
|
mkdir -p "$REPO_ROOT/captures"
|
||||||
|
out=$REPO_ROOT/captures/ft-test-${2:-WL-0}-$(date +%H%M%S).jpg
|
||||||
|
on_frame "screens/test/headless.sh $(printf '%q ' "$@")" > "$out"
|
||||||
|
echo "$out"
|
||||||
|
else
|
||||||
|
on_frame "screens/test/headless.sh $(printf '%q ' "$@")"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
rt=/run/user/$(id -u)/ft-test
|
||||||
|
cfg=/tmp/ft-test-config
|
||||||
|
bus=unix:path=$rt/bus
|
||||||
|
logs=/tmp/ft-test
|
||||||
|
mkdir -p "$logs"
|
||||||
|
|
||||||
|
kwin_env() {
|
||||||
|
env XDG_RUNTIME_DIR="$rt" XDG_CONFIG_HOME="$cfg" DBUS_SESSION_BUS_ADDRESS="$bus" \
|
||||||
|
WAYLAND_DISPLAY=wayland-0 QT_QPA_PLATFORM=wayland "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
ask() {
|
||||||
|
python3 - "$1" <<'EOF'
|
||||||
|
import socket, sys
|
||||||
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
s.bind("")
|
||||||
|
s.settimeout(2)
|
||||||
|
s.sendto(sys.argv[1].encode(), "\0ft_screens_test")
|
||||||
|
print(s.recv(8192).decode())
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
kd() { kwin_env kscreen-doctor "$@" 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; }
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
pkill -f -- "--control ft_screens_test" 2>/dev/null || true
|
||||||
|
[ -f "$rt/dbus.pid" ] && kill "$(cat "$rt/dbus.pid")" 2>/dev/null || true
|
||||||
|
sleep 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
load_script() { # file name
|
||||||
|
local id
|
||||||
|
id=$(kwin_env gdbus call --session -d org.kde.KWin -o /Scripting \
|
||||||
|
-m org.kde.kwin.Scripting.loadScript "$1" "$2" | tr -dc '0-9-')
|
||||||
|
[ "$id" -ge 0 ] 2>/dev/null || { echo "loadScript failed ($id)" >&2; return 1; }
|
||||||
|
kwin_env gdbus call --session -d org.kde.KWin -o "/Scripting/Script$id" -m org.kde.kwin.Script.run >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
unload_script() {
|
||||||
|
kwin_env gdbus call --session -d org.kde.KWin -o /Scripting -m org.kde.kwin.Scripting.unloadScript "$1" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start)
|
||||||
|
screens=${2:-2} spares=${3:-3}
|
||||||
|
stop
|
||||||
|
rm -rf "$rt" "$cfg"
|
||||||
|
mkdir -p "$rt" "$cfg"
|
||||||
|
chmod 700 "$rt"
|
||||||
|
args=(--screen 1920x1080@1.6 --screen 1080x1920@0.9)
|
||||||
|
for ((i = 2; i < screens; i++)); do args+=(--screen 1920x1080@1.6); done
|
||||||
|
args=("${args[@]:0:$((screens * 2))}")
|
||||||
|
for ((i = 0; i < spares; i++)); do args+=(--screen 800x600); done
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
nohup "$HOME/.local/bin/distrobox" enter "$FRAME_BOX" -- env XDG_RUNTIME_DIR="$rt" \
|
||||||
|
./screens/build/ft-screens --no-vr --socket ft-test-0 --control ft_screens_test "${args[@]}" \
|
||||||
|
> "$logs/screens.log" 2>&1 &
|
||||||
|
for _ in $(seq 100); do [ -S "$rt/ft-test-0" ] && break; sleep 0.2; done
|
||||||
|
[ -S "$rt/ft-test-0" ] || { echo "ft-screens didn't start:"; tail "$logs/screens.log"; exit 1; }
|
||||||
|
dbus-daemon --session --address="$bus" --fork --print-pid > "$rt/dbus.pid"
|
||||||
|
# KWin's screenshot interface without a permission check, so tests can look.
|
||||||
|
nohup env XDG_RUNTIME_DIR="$rt" XDG_CONFIG_HOME="$cfg" DBUS_SESSION_BUS_ADDRESS="$bus" \
|
||||||
|
WAYLAND_DISPLAY=ft-test-0 KWIN_SCREENSHOT_NO_PERMISSION_CHECKS=1 QT_LOGGING_RULES="kwin_scripting=true" QT_FORCE_STDERR_LOGGING=1 \
|
||||||
|
kwin_wayland --output-count $((screens + spares)) --width 800 --height 600 --no-lockscreen \
|
||||||
|
> "$logs/kwin.log" 2>&1 &
|
||||||
|
for _ in $(seq 100); do [ -S "$rt/wayland-0" ] && break; sleep 0.2; done
|
||||||
|
[ -S "$rt/wayland-0" ] || { echo "KWin didn't start:"; tail "$logs/kwin.log"; exit 1; }
|
||||||
|
sleep 1
|
||||||
|
off=()
|
||||||
|
for ((i = screens; i < screens + spares; i++)); do off+=("output.WL-$i.disable"); done
|
||||||
|
[ ${#off[@]} -eq 0 ] || kd "${off[@]}" >/dev/null
|
||||||
|
ask toplevels
|
||||||
|
;;
|
||||||
|
stop) stop ;;
|
||||||
|
ask) ask "$2" ;;
|
||||||
|
kd) shift; kd "$@" ;;
|
||||||
|
run)
|
||||||
|
shift
|
||||||
|
nohup env XDG_RUNTIME_DIR="$rt" XDG_CONFIG_HOME="$cfg" DBUS_SESSION_BUS_ADDRESS="$bus" \
|
||||||
|
WAYLAND_DISPLAY=wayland-0 QT_QPA_PLATFORM=wayland "$@" >> "$logs/apps.log" 2>&1 &
|
||||||
|
echo "started $1 (pid $!)"
|
||||||
|
;;
|
||||||
|
script) load_script "$(realpath "$2")" "${3:-$(basename "$2" .js)}" ;;
|
||||||
|
unscript) unload_script "$2" ;;
|
||||||
|
js)
|
||||||
|
f=$(mktemp /tmp/ft-test-XXXX.js)
|
||||||
|
printf '%s\n' "$2" > "$f"
|
||||||
|
name=ft-test-js-$$
|
||||||
|
mark=$(wc -l < "$logs/kwin.log")
|
||||||
|
load_script "$f" "$name"
|
||||||
|
sleep 0.5
|
||||||
|
unload_script "$name" || true
|
||||||
|
rm -f "$f"
|
||||||
|
tail -n +$((mark + 1)) "$logs/kwin.log" | sed -n 's/^js: //p'
|
||||||
|
;;
|
||||||
|
shot)
|
||||||
|
raw=$(mktemp /tmp/ft-test-shot.XXXXXX)
|
||||||
|
meta=$(kwin_env python3 - "${2:-WL-0}" "$raw" <<'PY'
|
||||||
|
import os, sys, dbus
|
||||||
|
name, raw = sys.argv[1:3]
|
||||||
|
bus = dbus.bus.BusConnection(os.environ['DBUS_SESSION_BUS_ADDRESS'])
|
||||||
|
shot = dbus.Interface(bus.get_object('org.kde.KWin', '/org/kde/KWin/ScreenShot2'), 'org.kde.KWin.ScreenShot2')
|
||||||
|
r, w = os.pipe()
|
||||||
|
fd = dbus.types.UnixFd(w)
|
||||||
|
os.close(w)
|
||||||
|
opts = dbus.Dictionary({'include-cursor': True, 'native-resolution': True}, signature='sv')
|
||||||
|
res = shot.CaptureScreen(name, opts, fd)
|
||||||
|
del fd # our copy of the write end, so the read ends when KWin is done
|
||||||
|
with os.fdopen(r, 'rb') as src, open(raw, 'wb') as dst:
|
||||||
|
while chunk := src.read(1 << 20):
|
||||||
|
dst.write(chunk)
|
||||||
|
print(int(res['width']), int(res['height']), int(res['stride']), int(res['format']))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
read -r width height stride format <<<"$meta"
|
||||||
|
layout=bgra # QImage formats 4-6: (A)RGB32, stored B,G,R,A; 16-18: RGBA8888
|
||||||
|
case $format in 16|17|18) layout=rgba ;; esac
|
||||||
|
"$HOME/.local/bin/distrobox" enter "$FRAME_BOX" -- magick -size "$((stride / 4))x$height" -depth 8 \
|
||||||
|
"$layout:$raw" -crop "${width}x${height}+0+0" +repage -alpha off -resize '1280x1280>' -quality 80 jpg:-
|
||||||
|
rm -f "$raw"
|
||||||
|
;;
|
||||||
|
log) tail -n "${3:-30}" "$logs/${2:-kwin}.log" ;;
|
||||||
|
*) sed -n '2,20p' "$0"; exit 2 ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// A KWin script for experiments (screens/test/headless.sh script ...): prints what happens
|
||||||
|
// to windows, for checking the script API floating windows rely on.
|
||||||
|
function desc(w) {
|
||||||
|
const g = w.frameGeometry;
|
||||||
|
return String(w.internalId).slice(1, 9) + " " + w.resourceClass + " pid=" + w.pid + " popup=" + w.popupWindow +
|
||||||
|
" transient=" + w.transient + " normal=" + w.normalWindow + " dialog=" + w.dialog +
|
||||||
|
" out=" + (w.output ? w.output.name : "?") + " geo=" + g.x + "," + g.y + " " + g.width + "x" + g.height +
|
||||||
|
" '" + w.caption + "'";
|
||||||
|
}
|
||||||
|
function watch(w) {
|
||||||
|
print("added " + desc(w));
|
||||||
|
w.interactiveMoveResizeStarted.connect(() => print("move-start move=" + w.move + " resize=" + w.resize + " " + desc(w)));
|
||||||
|
w.interactiveMoveResizeStepped.connect(g => print("move-step " + g.x + "," + g.y + " " + g.width + "x" + g.height));
|
||||||
|
w.interactiveMoveResizeFinished.connect(() => print("move-end " + desc(w)));
|
||||||
|
w.outputChanged.connect(() => print("output " + desc(w)));
|
||||||
|
w.fullScreenChanged.connect(() => print("fullscreen " + w.fullScreen + " " + desc(w)));
|
||||||
|
w.minimizedChanged.connect(() => print("minimized " + w.minimized));
|
||||||
|
}
|
||||||
|
workspace.windowList().forEach(watch);
|
||||||
|
workspace.windowAdded.connect(watch);
|
||||||
|
workspace.windowRemoved.connect(w => print("removed " + desc(w)));
|
||||||
|
workspace.windowActivated.connect(w => print("activated " + (w ? w.resourceClass : "none")));
|
||||||
+13
-1
@@ -206,6 +206,7 @@ constexpr double kRollStep = 5; // degrees per scroll notch on the roll but
|
|||||||
constexpr float kChromeIdle = 0.55f; // the controls' opacity without a laser on them
|
constexpr float kChromeIdle = 0.55f; // the controls' opacity without a laser on them
|
||||||
constexpr long kControlsLinger = 35; // ticks (~0.4 s) the controls stay after a laser leaves
|
constexpr long kControlsLinger = 35; // ticks (~0.4 s) the controls stay after a laser leaves
|
||||||
long g_tick = 0; // ft_vr_poll calls
|
long g_tick = 0; // ft_vr_poll calls
|
||||||
|
bool g_vr = false; // connected to SteamVR (ft-screens --no-vr runs without it)
|
||||||
constexpr vr::TrackedDeviceIndex_t kNone = vr::k_unTrackedDeviceIndexInvalid;
|
constexpr vr::TrackedDeviceIndex_t kNone = vr::k_unTrackedDeviceIndexInvalid;
|
||||||
|
|
||||||
struct Screen {
|
struct Screen {
|
||||||
@@ -1150,6 +1151,7 @@ bool ft_vr_init(void) {
|
|||||||
std::fprintf(stderr, "openvr: no IVRIPCResourceManagerClient (SteamVR too old?)\n");
|
std::fprintf(stderr, "openvr: no IVRIPCResourceManagerClient (SteamVR too old?)\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
g_vr = true;
|
||||||
RefreshPoses();
|
RefreshPoses();
|
||||||
// The catcher (see UpdateCatcher): clear and invisible, but the laser lands on it, and
|
// The catcher (see UpdateCatcher): clear and invisible, but the laser lands on it, and
|
||||||
// it keeps SteamVR's laser mouse on while it's up.
|
// it keeps SteamVR's laser mouse on while it's up.
|
||||||
@@ -1165,6 +1167,7 @@ bool ft_vr_init(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ft_vr_shutdown(void) {
|
void ft_vr_shutdown(void) {
|
||||||
|
if (!g_vr) return;
|
||||||
if (g_catcher != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->DestroyOverlay(g_catcher);
|
if (g_catcher != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->DestroyOverlay(g_catcher);
|
||||||
g_catcher = vr::k_ulOverlayHandleInvalid;
|
g_catcher = vr::k_ulOverlayHandleInvalid;
|
||||||
for (auto &[i, s] : g_screens)
|
for (auto &[i, s] : g_screens)
|
||||||
@@ -1179,14 +1182,20 @@ void ft_vr_shutdown(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
|
int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
|
||||||
|
if (!g_vr) { // --no-vr: nothing imports the buffers, so any layout KWin can draw
|
||||||
|
if (max < 1) return 0;
|
||||||
|
out[0] = 0; // DRM_FORMAT_MOD_LINEAR
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
uint32_t n = uint32_t(max);
|
uint32_t n = uint32_t(max);
|
||||||
if (!vr::VRIPCResourceManager()->GetDmabufModifiers(vr::VRApplication_Overlay, format, &n, out)) return 0;
|
if (!vr::VRIPCResourceManager()->GetDmabufModifiers(vr::VRApplication_Overlay, format, &n, out)) return 0;
|
||||||
return int(n < uint32_t(max) ? n : uint32_t(max));
|
return int(n < uint32_t(max) ? n : uint32_t(max));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ft_vr_screens_shown(void) { return ModeVisible(); }
|
bool ft_vr_screens_shown(void) { return g_vr && ModeVisible(); }
|
||||||
|
|
||||||
void ft_vr_screen_create(int index, double metres, int count) {
|
void ft_vr_screen_create(int index, double metres, int count) {
|
||||||
|
if (!g_vr) return;
|
||||||
Screen &s = g_screens[index];
|
Screen &s = g_screens[index];
|
||||||
s.metres = metres;
|
s.metres = metres;
|
||||||
char key[64], name[64];
|
char key[64], name[64];
|
||||||
@@ -1237,6 +1246,7 @@ void ft_vr_screen_destroy(int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *b) {
|
bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *b) {
|
||||||
|
if (!g_vr) return false;
|
||||||
auto sit = g_screens.find(index);
|
auto sit = g_screens.find(index);
|
||||||
if (sit == g_screens.end()) return false;
|
if (sit == g_screens.end()) return false;
|
||||||
Screen &s = sit->second;
|
Screen &s = sit->second;
|
||||||
@@ -1284,6 +1294,7 @@ void ft_vr_forget(const void *key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
||||||
|
if (!g_vr) return;
|
||||||
RefreshPoses();
|
RefreshPoses();
|
||||||
for (auto &[index, s] : g_screens) {
|
for (auto &[index, s] : g_screens) {
|
||||||
vr::VREvent_t ev;
|
vr::VREvent_t ev;
|
||||||
@@ -1438,6 +1449,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
|||||||
// (size <screen> <w> <h> and key <code> <value> are handled in compositor.c.) Screens are
|
// (size <screen> <w> <h> and key <code> <value> are handled in compositor.c.) Screens are
|
||||||
// numbered from 1 here, like everywhere the user sees them.
|
// numbered from 1 here, like everywhere the user sees them.
|
||||||
void ft_vr_command(const char *cmd, char *reply, int size) {
|
void ft_vr_command(const char *cmd, char *reply, int size) {
|
||||||
|
if (!g_vr) return (void)std::snprintf(reply, size, "error no SteamVR (--no-vr)");
|
||||||
RefreshPoses();
|
RefreshPoses();
|
||||||
int n;
|
int n;
|
||||||
double x, y, z, yaw, pitch, roll, w;
|
double x, y, z, yaw, pitch, roll, w;
|
||||||
|
|||||||
Reference in new issue
Block a user