fix: harden standalone overlay plane path on real DRM
Replace the per-frame overlay layer churn with one persistent layer per output (NULL buffer parks it), so libliftoff allocations settle instead of re-solving around a fresh layer object every frame. Repaint the held overlay from a per-frame content hash checked in the main-loop bodies: any drift schedules a frame, clean walks cost one cheap string pass. Deliberately not from the output frame handler -- scheduling there re-emits the same event via idle and wedges the loop at 100% CPU with dead stdin (caught via gdb). Drop all scattered explicit overlay refresh calls; the confirm-quit modal stays on its explicit damage path. Cover transient plane rejects with one composite frame then retry the plane; lock software cursors while the overlay is held on DRM so the menu layer can never trade planes with the cursor across re-solves. Log the plane verdict and box (WxH+X+Y) every plane frame as the off-process handle for VT verification. Add tests/test_vt_plane.py: foot + Alt-held probe + Alt+Shift+T alacritty on the real DRM backend, asserting the layer stays accepted and the menu FB sits topmost at the logged geometry (libdrm plane reads plus debugfs crtc-pos/zpos), with cursor warps and held-key repeats churning the allocation.
This commit is contained in:
parent
137e4d0568
commit
9ea77fde6e
2 changed files with 738 additions and 68 deletions
216
kick.c
216
kick.c
|
|
@ -386,10 +386,11 @@ struct kick_server {
|
|||
wlr_output_layer when the backend accepts it (libliftoff assigns it a
|
||||
plane), so the main buffer never carries the overlay and direct scanout
|
||||
of the focused client stays live underneath. */
|
||||
struct wlr_output_layer *overlay_layer;
|
||||
struct wlr_output_layer *overlay_layer; /* persistent, per output */
|
||||
struct wlr_swapchain *overlay_swapchain;
|
||||
/* Output-buffer-local box the overlay was last painted into on the plane.
|
||||
Kept so a buffer exactly sized to it can be (re)created. */
|
||||
/* Last committed state of the persistent layer. buffer is NULL while
|
||||
the menu is hidden (the plane parked); the entry is described on
|
||||
every commit so the backend's layer set never churns. */
|
||||
struct wlr_output_layer_state overlay_layer_state;
|
||||
/* Plane capability, probed once per output: the first overlay hold asks
|
||||
the plane with a real commit and records whether libliftoff accepted
|
||||
|
|
@ -401,6 +402,12 @@ struct kick_server {
|
|||
OVERLAY_PLANE_OFF, /* hardware/knob: composite always */
|
||||
} overlay_plane;
|
||||
bool overlay_render_lock; /* attach-render lock held for the fallback */
|
||||
bool overlay_cursor_lock; /* software-cursor lock held while up (DRM) */
|
||||
/* One composite cover frame after a rejected plane frame (see
|
||||
render_standalone): the next attempt retries the plane, so a
|
||||
transient allocation shift costs one composited frame, not a stuck
|
||||
menu. Cleared by the cover frame itself and on overlay hide. */
|
||||
bool overlay_composite_next;
|
||||
/* Standalone-only cursor: a real wlr_cursor so the pointer is visible
|
||||
(hardware cursor plane on DRM, scene-composited on headless). */
|
||||
struct wlr_cursor *cursor;
|
||||
|
|
@ -1022,6 +1029,7 @@ static void set_active_tab(struct kick_tab *tab);
|
|||
static void apply_window_priorities(struct kick_window *active);
|
||||
static void render_and_present(void);
|
||||
static void overlay_sync_render_lock(void);
|
||||
static void overlay_sync_cursor_lock(void);
|
||||
/* Backstop for overlay staleness (see overlay_poll_check): hash what the
|
||||
tab overlay would draw, and record what was last painted. */
|
||||
static uint64_t overlay_content_hash(void);
|
||||
|
|
@ -3093,9 +3101,14 @@ static void apply_pending_output_resize(struct wlr_output_state *state) {
|
|||
static void handle_output_frame(struct wl_listener *listener, void *data) {
|
||||
(void)listener; (void)data;
|
||||
frame_pending = true;
|
||||
/* Display-rate backstop on real outputs: notice overlay drift every
|
||||
frame even if no mutation site scheduled a repaint. */
|
||||
overlay_poll_check();
|
||||
/* NOTE: no overlay_poll_check() here by design. kick_request_redraw()
|
||||
schedules through wlr_output_schedule_frame, which re-emits this very
|
||||
frame event via an idle source: polling from inside the handler would
|
||||
re-arm idle sources faster than wl_event_loop_dispatch_idle drains
|
||||
them while any drift persists (the render gate that would clear it
|
||||
never runs), wedging the loop at 100% CPU with dead stdin and
|
||||
broken client sockets. The backstop lives in the main-loop bodies
|
||||
instead, where each check is followed by the render gate. */
|
||||
}
|
||||
|
||||
/* How many standalone outputs have been seen since the last (re)connect.
|
||||
|
|
@ -3114,12 +3127,18 @@ static void handle_output_destroy(struct wl_listener *listener, void *data) {
|
|||
wl_list_remove(&server.frame.link);
|
||||
server.output = NULL;
|
||||
server.scene_output = NULL; /* wlroots frees it via its output addon */
|
||||
/* The overlay layer dies with the output (wlroots frees output->layers);
|
||||
the box-sized swapchain is ours and must go. Re-probe the plane on the
|
||||
next output. */
|
||||
server.overlay_layer = NULL;
|
||||
/* The overlay layer dies with the output (wlroots frees output->layers,
|
||||
so destroy our handle too); the box-sized swapchain is ours and must
|
||||
go. Re-probe the plane on the next output. */
|
||||
if (server.overlay_layer) {
|
||||
wlr_output_layer_destroy(server.overlay_layer);
|
||||
server.overlay_layer = NULL;
|
||||
}
|
||||
server.overlay_layer_state = (struct wlr_output_layer_state){0};
|
||||
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
|
||||
server.overlay_composite_next = false;
|
||||
server.overlay_render_lock = false;
|
||||
server.overlay_cursor_lock = false;
|
||||
if (server.overlay_swapchain) {
|
||||
wlr_swapchain_destroy(server.overlay_swapchain);
|
||||
server.overlay_swapchain = NULL;
|
||||
|
|
@ -3172,21 +3191,25 @@ server.output = output;
|
|||
if (server.standalone && wlr_output_is_headless(output))
|
||||
wlr_output_lock_software_cursors(output, true);
|
||||
|
||||
/* Standalone DRM output: the overlay gets its own wlr_output_layer on
|
||||
demand. libliftoff assigns it a real hardware plane when the driver
|
||||
has one spare; the headless backend would claim every layer accepted
|
||||
but never draw it, so it gets no layer and stays on the composite
|
||||
path. The layer is created per frame in render_standalone and
|
||||
destroyed at the end of that frame, so a layer only exists in the
|
||||
backend's layer set while the overlay is actually being presented --
|
||||
hiding the menu destroys the layer and libliftoff must drop its
|
||||
plane, which a NULL-buffer layer would silently keep alive. */
|
||||
/* Standalone DRM output: the overlay gets one persistent wlr_output_layer
|
||||
for the output's lifetime. libliftoff assigns it a real hardware
|
||||
plane when the driver has one spare; keeping the same layer across
|
||||
frames lets the allocation settle instead of re-solving on every
|
||||
frame. The headless backend would claim every layer accepted but
|
||||
never draw it, so it gets no layer and stays on the composite path.
|
||||
Hiding the menu commits the layer with a NULL buffer, which clears
|
||||
the plane (FB_ID 0); the layer object itself stays put. */
|
||||
if (server.standalone && !server.standalone_headless) {
|
||||
server.overlay_plane = OVERLAY_PLANE_UNKNOWN;
|
||||
server.overlay_layer = wlr_output_layer_create(output);
|
||||
server.overlay_layer_state = (struct wlr_output_layer_state){
|
||||
.layer = NULL,
|
||||
.layer = server.overlay_layer,
|
||||
.buffer = NULL,
|
||||
};
|
||||
if (!server.overlay_layer) {
|
||||
kick_log(KICK_LOG_DEBUG,
|
||||
"[render] overlay layer create failed, compositing\n");
|
||||
}
|
||||
if (server.overlay_swapchain) {
|
||||
wlr_swapchain_destroy(server.overlay_swapchain);
|
||||
server.overlay_swapchain = NULL;
|
||||
|
|
@ -3198,7 +3221,9 @@ server.output = output;
|
|||
plane capability (UNKNOWN on a fresh output tries the plane, so no
|
||||
lock) and covers both the tab list and the confirm-quit modal. */
|
||||
server.overlay_render_lock = false;
|
||||
server.overlay_cursor_lock = false;
|
||||
overlay_sync_render_lock();
|
||||
overlay_sync_cursor_lock();
|
||||
|
||||
/* Give the standalone cursor a place to live. Idempotent across VT
|
||||
round-trips (wlr_output_layout_add reuses the existing entry), and the
|
||||
|
|
@ -8794,6 +8819,7 @@ static void set_confirm_quit(bool on) {
|
|||
tab list); on the composite path it must repaint into our own buffer,
|
||||
so the scanout lock follows whether the plane is usable. */
|
||||
overlay_sync_render_lock();
|
||||
overlay_sync_cursor_lock();
|
||||
if (server.standalone)
|
||||
standalone_overlay_damage();
|
||||
else if (server.scene_output)
|
||||
|
|
@ -9479,14 +9505,20 @@ static void update_overlay_visible(bool alt) {
|
|||
kick_log(KICK_LOG_DEBUG, "[render] overlay %s\n",
|
||||
alt ? "show" : "hide");
|
||||
/* A new hold paints fresh content on its first frame; until then the
|
||||
poll backstop must not compare against a previous hold. */
|
||||
poll backstop must not compare against a previous hold. A stale
|
||||
composite cover flag dies here too so the hold starts on the plane. */
|
||||
overlay_painted_valid = false;
|
||||
server.overlay_composite_next = false;
|
||||
/* Scanout lock ownership: when the overlay rides a hardware plane the lock
|
||||
must stay off so direct scanout of the focused client keeps running
|
||||
underneath; otherwise it must be held for the whole hold so the second
|
||||
paint pass lands on our own composited buffer. Releasing it re-enables
|
||||
scanout automatically (same pattern labwc uses for its panels). */
|
||||
overlay_sync_render_lock();
|
||||
/* The hardware cursor plane is parked for the hold as well, so the
|
||||
overlay layer can never trade places with the cursor across
|
||||
libliftoff re-solves (see overlay_sync_cursor_lock). */
|
||||
overlay_sync_cursor_lock();
|
||||
/* The overlay is baked into the scene output's swapchain buffers in both
|
||||
modes now, so a transition must force those buffers to repaint from
|
||||
clean scene content: standalone via a whole-output repaint, nested via
|
||||
|
|
@ -12121,8 +12153,8 @@ static bool overlay_plane_knob(void) {
|
|||
/* May this frame try the plane? USED holds are the settled path; UNKNOWN
|
||||
means the very next overlay frame is the one-shot probe that decides.
|
||||
OFF (headless, nested, knob turned down, or a failed probe) means
|
||||
composite always. The layer itself is created and destroyed per frame
|
||||
(in render_standalone), so the idempotent checks below never gate on it. */
|
||||
composite always. Whether the persistent layer object exists never
|
||||
gates this: creation failure simply degrades to composite. */
|
||||
static bool overlay_plane_capable(void) {
|
||||
return server.standalone && !server.standalone_headless &&
|
||||
overlay_plane_knob() &&
|
||||
|
|
@ -12149,6 +12181,29 @@ static void overlay_sync_render_lock(void) {
|
|||
overlay_set_render_lock(up && !overlay_plane_capable());
|
||||
}
|
||||
|
||||
/* Take or release the software-cursor lock while the overlay is up on a
|
||||
real DRM output. With a hardware cursor plane in play, libliftoff is
|
||||
free to trade planes across re-solves -- parking our menu layer on the
|
||||
cursor plane (and the cursor on an overlay plane), where size limits
|
||||
can crop it into invisibility. Locking software cursors removes the
|
||||
cursor layer from the allocation entirely while held, so the only
|
||||
layers left are the primary and ours: ours always lands on an overlay
|
||||
plane. Idempotent; headless outputs are already locked permanently at
|
||||
output init and never touched here. */
|
||||
static void overlay_set_cursor_lock(bool want) {
|
||||
if (want == server.overlay_cursor_lock) return;
|
||||
if (server.output && server.standalone && !server.standalone_headless)
|
||||
wlr_output_lock_software_cursors(server.output, want);
|
||||
server.overlay_cursor_lock = want;
|
||||
}
|
||||
|
||||
/* Recompute the cursor-lock state alongside the render lock: held whenever
|
||||
the overlay (tab list or modal) is up on DRM. Called from the same
|
||||
three places as overlay_sync_render_lock. */
|
||||
static void overlay_sync_cursor_lock(void) {
|
||||
overlay_set_cursor_lock(host.overlay_visible || confirm_quit_pending);
|
||||
}
|
||||
|
||||
/* (Re)create the box-sized overlay swapchain. The overlay is opaque, so
|
||||
XRGB8888 (no alpha) is the plane format; a plane can only ever use a
|
||||
format it can actually scan out, and if none fits libliftoff reports the
|
||||
|
|
@ -12205,51 +12260,53 @@ static void render_standalone(void) {
|
|||
double t_build = 0, t_commit = 0;
|
||||
bool frame_committed = false;
|
||||
|
||||
/* Plane path: paint the overlay into its own small buffer and ask the
|
||||
backend (libliftoff, on DRM) to scan it out on a hardware plane for
|
||||
this frame. The layer is created fresh for THIS frame only and
|
||||
destroyed after wlr_output_state_finish(), so the backend's layer set
|
||||
exactly mirrors what we presented. The first hold of each output is
|
||||
the one-shot probe: the commit tells us whether libliftoff accepted
|
||||
the layer, which becomes the session answer. Every later frame just
|
||||
asks (layer with a buffer) or does not (no layer at all when the menu
|
||||
is hidden; the layer_state array must stay valid until finish, so the
|
||||
layer is destroyed only after finish). */
|
||||
/* Plane path: paint the overlay into its own small buffer and present
|
||||
it on the output's persistent layer, which libliftoff scans out on a
|
||||
hardware plane. The layer lives as long as the output; every commit
|
||||
carries its state (a NULL buffer disables it when the menu is
|
||||
hidden), so the backend's allocation settles instead of re-solving
|
||||
around a fresh layer object on every frame. The first hold of each
|
||||
output is the one-shot probe: the commit tells us whether libliftoff
|
||||
accepted the layer, which becomes the session answer. */
|
||||
struct overlay_box box;
|
||||
bool have_box = false;
|
||||
struct wlr_buffer *plane_buf = NULL;
|
||||
bool plane_attempt = overlay_up && overlay_plane_capable();
|
||||
/* A rejected plane frame sets composite_next for one composite cover
|
||||
frame, then retries the plane: with a stable allocation rejects are
|
||||
transient, so flap back instead of giving up. */
|
||||
bool plane_attempt = overlay_up && overlay_plane_capable() &&
|
||||
server.overlay_layer && !server.overlay_composite_next;
|
||||
if (kick_log_level() >= KICK_LOG_DEBUG && server.standalone)
|
||||
kick_log(KICK_LOG_DEBUG, "[diag] frame start t=%.0f vis=%d plane=%d\n",
|
||||
kick_monotonic_ms(), overlay_up, plane_attempt);
|
||||
struct wlr_output_layer *frame_layer = NULL;
|
||||
if (plane_attempt) {
|
||||
int nrows = confirm_quit_pending ? overlay_confirm_box(&box)
|
||||
: overlay_tab_box(&box);
|
||||
if (nrows > 0) {
|
||||
overlay_plane_resize(box.w, box.h);
|
||||
plane_buf = overlay_render_plane(&box);
|
||||
have_box = plane_buf != NULL;
|
||||
}
|
||||
plane_attempt = plane_buf != NULL;
|
||||
plane_attempt = have_box;
|
||||
}
|
||||
if (plane_attempt) {
|
||||
frame_layer = wlr_output_layer_create(server.output);
|
||||
if (!frame_layer) {
|
||||
plane_attempt = false;
|
||||
wlr_buffer_unlock(plane_buf);
|
||||
plane_buf = NULL;
|
||||
} else {
|
||||
server.overlay_layer = frame_layer;
|
||||
server.overlay_layer_state.layer = frame_layer;
|
||||
server.overlay_layer_state.buffer = plane_buf;
|
||||
if (server.overlay_layer) {
|
||||
/* Always describe the one persistent layer: with a buffer while
|
||||
asking for the plane, NULL to park it while hidden (FB_ID 0
|
||||
drops the plane). The array must stay valid until finish. */
|
||||
server.overlay_layer_state.layer = server.overlay_layer;
|
||||
server.overlay_layer_state.buffer = plane_buf;
|
||||
if (have_box) {
|
||||
server.overlay_layer_state.src_box = (struct wlr_fbox){
|
||||
.x = 0, .y = 0, .width = box.w, .height = box.h };
|
||||
server.overlay_layer_state.dst_box = (struct wlr_box){
|
||||
.x = box.x, .y = box.y, .width = box.w, .height = box.h };
|
||||
server.overlay_layer_state.damage = NULL;
|
||||
} else {
|
||||
server.overlay_layer_state.src_box = (struct wlr_fbox){0};
|
||||
server.overlay_layer_state.dst_box = (struct wlr_box){0};
|
||||
}
|
||||
}
|
||||
if (frame_layer)
|
||||
server.overlay_layer_state.damage = NULL;
|
||||
wlr_output_state_set_layers(&state, &server.overlay_layer_state, 1);
|
||||
}
|
||||
|
||||
/* Composite path: the overlay joins the scene's own buffer. Its box is
|
||||
invisible to the scene's damage ring, so while it is up every frame
|
||||
|
|
@ -12274,19 +12331,15 @@ static void render_standalone(void) {
|
|||
scans out (planes persist across frames); every overlay show/hide
|
||||
forces a repaint through standalone_overlay_damage(), so a menu
|
||||
appearing or clearing always goes out on a rendered frame.
|
||||
Drop the plane resources we acquired for this frame before
|
||||
finishing the (empty) state exactly once. */
|
||||
Drop this frame's plane buffer before finishing the (empty)
|
||||
state exactly once; the persistent layer entry stays described
|
||||
(with this buffer still attached) so the next attempt reuses the
|
||||
settled allocation. */
|
||||
kick_log(KICK_LOG_DEBUG, "[render] build_state failed\n");
|
||||
if (frame_layer) {
|
||||
wlr_output_layer_destroy(frame_layer);
|
||||
server.overlay_layer = NULL;
|
||||
server.overlay_layer_state.layer = NULL;
|
||||
server.overlay_layer_state.buffer = NULL;
|
||||
frame_layer = NULL;
|
||||
}
|
||||
if (plane_buf) {
|
||||
wlr_buffer_unlock(plane_buf);
|
||||
plane_buf = NULL;
|
||||
server.overlay_layer_state.buffer = NULL;
|
||||
}
|
||||
plane_attempt = false;
|
||||
goto frame_done;
|
||||
|
|
@ -12343,6 +12396,21 @@ static void render_standalone(void) {
|
|||
}
|
||||
output_mgmt_sync();
|
||||
|
||||
/* Witness for the overlay plane: whether libliftoff placed this frame's
|
||||
layer, plus the box it was asked to scan out (WxH+X+Y in output
|
||||
pixels). KMS offers no per-layer name, so the box -- which sizes the
|
||||
plane's FB exactly -- is the off-process handle: a VT test matches
|
||||
the live kernel plane by FB size and hold/release timing. */
|
||||
if (plane_attempt) {
|
||||
kick_log(KICK_LOG_DEBUG, "[render] overlay plane accepted=%d "
|
||||
"box=%dx%d+%d+%d layer=%p buf=%dx%d\n",
|
||||
server.overlay_layer_state.accepted ? 1 : 0,
|
||||
box.w, box.h, box.x, box.y,
|
||||
(void *)server.overlay_layer,
|
||||
plane_buf ? plane_buf->width : 0,
|
||||
plane_buf ? plane_buf->height : 0);
|
||||
}
|
||||
|
||||
if (plane_attempt && server.overlay_plane == OVERLAY_PLANE_UNKNOWN) {
|
||||
/* One-shot probe: the accepted flag is only populated by a real
|
||||
commit -- libliftoff never writes it on a test-only path -- so this
|
||||
|
|
@ -12351,6 +12419,7 @@ static void render_standalone(void) {
|
|||
if (server.overlay_layer_state.accepted) {
|
||||
kick_log(KICK_LOG_DEBUG, "[render] overlay plane available\n");
|
||||
server.overlay_plane = OVERLAY_PLANE_USED;
|
||||
overlay_note_painted();
|
||||
} else {
|
||||
kick_log(KICK_LOG_DEBUG,
|
||||
"[render] overlay plane not available, compositing\n");
|
||||
|
|
@ -12359,21 +12428,32 @@ static void render_standalone(void) {
|
|||
the menu appear anyway by re-rendering it composited. */
|
||||
kick_request_redraw();
|
||||
}
|
||||
} else if (plane_attempt) {
|
||||
if (server.overlay_layer_state.accepted) {
|
||||
overlay_note_painted();
|
||||
} else {
|
||||
/* Transiently rejected (allocation shifted under us): cover
|
||||
this frame composited, then retry the plane next frame.
|
||||
The lock must be held for the cover frame, and the next
|
||||
attempt reuses the settled layer. */
|
||||
kick_log(KICK_LOG_DEBUG,
|
||||
"[render] overlay plane rejected, compositing once\n");
|
||||
server.overlay_composite_next = true;
|
||||
overlay_set_render_lock(true);
|
||||
kick_request_redraw();
|
||||
}
|
||||
}
|
||||
if (server.overlay_composite_next && composite_overlay) {
|
||||
/* The cover frame above painted composited: retry the plane. */
|
||||
server.overlay_composite_next = false;
|
||||
}
|
||||
|
||||
frame_done:
|
||||
/* Finish the output state first: the layers array must stay valid until
|
||||
finish returns. Only then destroy the per-frame layer (removing the
|
||||
Liftoff layer from the CRTC so the next allocation drops the plane)
|
||||
and unlock the plane buffer that the commit just consumed. */
|
||||
finish returns. The layer itself persists across frames (created with
|
||||
the output); only unlock this frame's plane buffer, which the commit
|
||||
just consumed. */
|
||||
wlr_output_state_finish(&state);
|
||||
if (frame_layer) {
|
||||
wlr_output_layer_destroy(frame_layer);
|
||||
server.overlay_layer = NULL;
|
||||
server.overlay_layer_state.layer = NULL;
|
||||
server.overlay_layer_state.buffer = NULL;
|
||||
frame_layer = NULL;
|
||||
}
|
||||
if (plane_buf) {
|
||||
wlr_buffer_unlock(plane_buf);
|
||||
plane_buf = NULL;
|
||||
|
|
|
|||
590
tests/test_vt_plane.py
Normal file
590
tests/test_vt_plane.py
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
#!/usr/bin/env python3
|
||||
"""VT overlay-plane test: the menu must survive a client format switch.
|
||||
|
||||
Spawns kick on the real DRM backend with a `foot` tab, holds Alt (the
|
||||
overlay plane is probed), then Alt+Shift+T's an `alacritty` tab while
|
||||
still holding Alt. foot uses shm buffers, alacritty GL/dmabuf: if the
|
||||
overlay layer stops being accepted when alacritty's buffers join the
|
||||
libliftoff allocation, the menu vanishes over alacritty but not foot.
|
||||
Every plane frame logs `[render] overlay plane accepted=0/1`, so the log
|
||||
is the witness (plane content never reaches frame dumps).
|
||||
|
||||
Skips unless this process can grab DRM master on a card with a connected
|
||||
connector (i.e. a live virtual console). Taking master steals the live
|
||||
display for the duration; master is dropped on teardown.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import glob
|
||||
import ctypes
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from test_smoke import ( # noqa: F401
|
||||
ALT,
|
||||
BIN,
|
||||
DOWN,
|
||||
KEY_PRESSED,
|
||||
KEY_RELEASED,
|
||||
SHIFT,
|
||||
UP,
|
||||
Session,
|
||||
_assert_alive,
|
||||
_fresh_log,
|
||||
_mk_dump_dir,
|
||||
_pick_font,
|
||||
_preexec_death,
|
||||
)
|
||||
|
||||
_DRM_SET_MASTER = 0x641E
|
||||
_DRM_DROP_MASTER = 0x641F
|
||||
|
||||
_VT_LOCK = __file__ + ".lock"
|
||||
|
||||
re_frames = re.compile(
|
||||
r"\[diag\] frame (?:start|result) .*?vis=(\d) plane=(\d)")
|
||||
re_accept = re.compile(r"\[render\] overlay plane accepted=([01])")
|
||||
|
||||
|
||||
def _drm_card_with_connected():
|
||||
for card in sorted(glob.glob("/dev/dri/card[0-9]*")):
|
||||
sysf = card.replace("/dev/dri/", "/sys/class/drm/")
|
||||
for status in glob.glob(sysf + "/*/status"):
|
||||
try:
|
||||
if open(status).read().strip() == "connected":
|
||||
return card
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _vt_drm_available():
|
||||
card = _drm_card_with_connected()
|
||||
if not card:
|
||||
return False
|
||||
try:
|
||||
fd = os.open(card, os.O_RDWR)
|
||||
except OSError:
|
||||
return False
|
||||
try:
|
||||
fcntl.ioctl(fd, _DRM_SET_MASTER, 0)
|
||||
except OSError:
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
fcntl.ioctl(fd, _DRM_DROP_MASTER, 0)
|
||||
except OSError:
|
||||
pass
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
|
||||
def _liftoff_excerpt(log, n=3000):
|
||||
lines = [l for l in log.splitlines()
|
||||
if "libliftoff" in l or "overlay plane" in l]
|
||||
return "\n".join(lines[-80:]) + "\n...[last %d chars]\n%s" % (n, log[-n:])
|
||||
|
||||
|
||||
def _drm_planes(card="/dev/dri/card1"):
|
||||
"""Kernel-side plane state via libdrm (no master needed, read-only).
|
||||
|
||||
Returns [(id, type, crtc_id, fb_id, fb_w, fb_h, zpos)]. KMS has no
|
||||
per-layer name, so the overlay menu is identified by what only it can
|
||||
be while we hold Alt: an active overlay plane whose FB is exactly the
|
||||
box size kick logged (`overlay plane box=WxH+X+Y`), timed to appear on
|
||||
hold and vanish on release. Client buffers always go to the primary;
|
||||
the hardware cursor lives on its own non-overlay plane.
|
||||
"""
|
||||
lib = ctypes.CDLL("libdrm.so.2")
|
||||
|
||||
class PlaneRes(ctypes.Structure):
|
||||
_fields_ = [("count_planes", ctypes.c_uint32),
|
||||
("planes", ctypes.POINTER(ctypes.c_uint32))]
|
||||
|
||||
class Plane(ctypes.Structure):
|
||||
_fields_ = [("count_formats", ctypes.c_uint32),
|
||||
("formats", ctypes.POINTER(ctypes.c_uint32)),
|
||||
("plane_id", ctypes.c_uint32),
|
||||
("crtc_id", ctypes.c_uint32),
|
||||
("fb_id", ctypes.c_uint32),
|
||||
("crtc_x", ctypes.c_int32), ("crtc_y", ctypes.c_int32),
|
||||
("x", ctypes.c_uint32), ("y", ctypes.c_uint32),
|
||||
("possible_crtcs", ctypes.c_uint32),
|
||||
("gamma_size", ctypes.c_uint32)]
|
||||
|
||||
class PropPtrs(ctypes.Structure):
|
||||
_fields_ = [("count_props", ctypes.c_uint32),
|
||||
("props", ctypes.POINTER(ctypes.c_uint32)),
|
||||
("prop_values", ctypes.POINTER(ctypes.c_uint64))]
|
||||
|
||||
class Prop(ctypes.Structure):
|
||||
_fields_ = [("prop_id", ctypes.c_uint32), ("flags", ctypes.c_uint32),
|
||||
("name", ctypes.c_char * 32),
|
||||
("count_values", ctypes.c_uint32),
|
||||
("values", ctypes.POINTER(ctypes.c_uint64))]
|
||||
|
||||
class FB(ctypes.Structure):
|
||||
_fields_ = [("fb_id", ctypes.c_uint32),
|
||||
("width", ctypes.c_uint32), ("height", ctypes.c_uint32),
|
||||
("pitch", ctypes.c_uint32), ("bpp", ctypes.c_uint32),
|
||||
("depth", ctypes.c_uint32), ("handle", ctypes.c_uint32)]
|
||||
|
||||
lib.drmModeGetPlaneResources.restype = ctypes.POINTER(PlaneRes)
|
||||
lib.drmModeGetPlaneResources.argtypes = [ctypes.c_int]
|
||||
lib.drmModeGetPlane.restype = ctypes.POINTER(Plane)
|
||||
lib.drmModeGetPlane.argtypes = [ctypes.c_int, ctypes.c_uint32]
|
||||
lib.drmModeObjectGetProperties.restype = ctypes.POINTER(PropPtrs)
|
||||
lib.drmModeObjectGetProperties.argtypes = [ctypes.c_int, ctypes.c_uint32,
|
||||
ctypes.c_uint32]
|
||||
lib.drmModeGetProperty.restype = ctypes.POINTER(Prop)
|
||||
lib.drmModeGetProperty.argtypes = [ctypes.c_int, ctypes.c_uint32]
|
||||
lib.drmModeGetFB.restype = ctypes.POINTER(FB)
|
||||
lib.drmModeGetFB.argtypes = [ctypes.c_int, ctypes.c_uint32]
|
||||
lib.drmModeFreePlaneResources.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreePlane.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreeObjectProperties.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreeProperty.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreeFB.argtypes = [ctypes.c_void_p]
|
||||
|
||||
out = []
|
||||
fd = os.open(card, os.O_RDWR)
|
||||
try:
|
||||
pres = lib.drmModeGetPlaneResources(fd)
|
||||
ids = [pres.contents.planes[i]
|
||||
for i in range(pres.contents.count_planes)]
|
||||
lib.drmModeFreePlaneResources(pres)
|
||||
for pid in ids:
|
||||
pl = lib.drmModeGetPlane(fd, pid)
|
||||
c = pl.contents
|
||||
zpos = None
|
||||
ptype = None
|
||||
pp = lib.drmModeObjectGetProperties(fd, pid, 0xeeeeeeee)
|
||||
if pp:
|
||||
for j in range(pp.contents.count_props):
|
||||
prid = pp.contents.props[j]
|
||||
pr = lib.drmModeGetProperty(fd, prid)
|
||||
name = pr.contents.name.decode()
|
||||
if name == "zpos":
|
||||
zpos = pp.contents.prop_values[j]
|
||||
elif name == "type":
|
||||
ptype = pp.contents.prop_values[j]
|
||||
lib.drmModeFreeProperty(pr)
|
||||
lib.drmModeFreeObjectProperties(pp)
|
||||
fw = fh = None
|
||||
if c.fb_id:
|
||||
fb = lib.drmModeGetFB(fd, c.fb_id)
|
||||
if fb:
|
||||
fw, fh = fb.contents.width, fb.contents.height
|
||||
lib.drmModeFreeFB(fb)
|
||||
out.append((c.plane_id, ptype, c.crtc_id, c.fb_id, fw, fh,
|
||||
zpos))
|
||||
lib.drmModeFreePlane(pl)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return out
|
||||
|
||||
|
||||
def _logged_box(s):
|
||||
"""Latest `box=WxH+X+Y` from the overlay plane witness lines, or None."""
|
||||
ms = re.findall(r"overlay plane accepted=[01] box=(\d+)x(\d+)\+(\d+)\+(\d+)",
|
||||
s.read_log())
|
||||
return tuple(map(int, ms[-1])) if ms else None
|
||||
|
||||
|
||||
def _debugfs_planes(path="/sys/kernel/debug/dri/1/state"):
|
||||
"""Kernel atomic state: {plane_id: {crtc, fb, cw, ch, cx, cy, zpos,
|
||||
fb_w, fb_h}}. Unlike the ioctl queries this shows the PROGRAMMED
|
||||
destination rectangle and z-order -- the ground truth for whether our
|
||||
menu actually lands centered and on top."""
|
||||
planes = {}
|
||||
try:
|
||||
text = open(path).read()
|
||||
except OSError:
|
||||
return planes
|
||||
cur = None
|
||||
import re as _re
|
||||
|
||||
def _ints(line):
|
||||
return list(map(int, _re.findall(r"\d+", line)))
|
||||
|
||||
for line in text.splitlines():
|
||||
m = _re.match(r"plane\[(\d+)\]:", line)
|
||||
if m:
|
||||
cur = int(m.group(1))
|
||||
planes[cur] = {}
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("crtc="):
|
||||
v = line[5:]
|
||||
planes[cur]["crtc"] = None if v == "(null)" else v
|
||||
elif line.startswith("fb="):
|
||||
nums = _ints(line[3:])
|
||||
planes[cur]["fb"] = nums[0] if nums else 0
|
||||
elif line.startswith("crtc-pos="):
|
||||
nums = _ints(line)
|
||||
if len(nums) >= 4:
|
||||
planes[cur]["cw"], planes[cur]["ch"], planes[cur]["cx"], \
|
||||
planes[cur]["cy"] = nums[:4]
|
||||
elif line.startswith("normalized-zpos="):
|
||||
nums = _ints(line)
|
||||
if nums:
|
||||
planes[cur]["zpos"] = nums[0]
|
||||
elif line.startswith("size=") and "fb_w" not in planes[cur]:
|
||||
nums = _ints(line)
|
||||
if len(nums) >= 2:
|
||||
planes[cur]["fb_w"], planes[cur]["fb_h"] = nums[:2]
|
||||
return planes
|
||||
|
||||
|
||||
def _assert_menu_placed(card, box, ctx, samples=3):
|
||||
"""The menu FB must be programmed at the logged box geometry on top:
|
||||
same CRTC as the fullscreen primary, crtc-pos == box, zpos above the
|
||||
primary's. Accepted/size checks can't see misplacement -- this can."""
|
||||
if box is None:
|
||||
raise AssertionError("no logged overlay box (%s)" % ctx)
|
||||
bw, bh, bx, by_ = box
|
||||
for _ in range(samples):
|
||||
planes = _debugfs_planes()
|
||||
prims = [p for p in planes.values()
|
||||
if p.get("crtc") and p.get("fb")
|
||||
and p.get("fb_w", 0) >= 1920 and p.get("zpos") == 0]
|
||||
if not prims:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
crtc = prims[0]["crtc"]
|
||||
menu = [pid for pid, p in planes.items()
|
||||
if p.get("crtc") == crtc and p.get("fb")
|
||||
and p.get("fb_w") == bw and p.get("fb_h") == bh
|
||||
and p.get("cx") == bx and p.get("cy") == by_
|
||||
and p.get("zpos", 0) > 0]
|
||||
if len(menu) == 1:
|
||||
print("menu placed: plane %s at %dx%d+%d+%d zpos=%s (%s)"
|
||||
% (menu[0], bw, bh, bx, by_,
|
||||
planes[menu[0]].get("zpos"), ctx))
|
||||
return menu[0]
|
||||
time.sleep(0.2)
|
||||
raise AssertionError(
|
||||
"menu not placed at %dx%d+%d+%d (%s); planes:\n%s" % (
|
||||
bw, bh, bx, by_, ctx,
|
||||
"\n".join("%s: %s" % (pid, p)
|
||||
for pid, p in sorted(planes.items())
|
||||
if p.get("crtc"))))
|
||||
|
||||
|
||||
def _menu_plane_sample(card, box):
|
||||
"""One kernel snapshot: the plane id carrying the menu-size FB, or
|
||||
None. libliftoff may place our layer on any compatible plane
|
||||
(overlay 107, but also 83/657 in a pinch), so every candidate is
|
||||
scanned: active CRTC, live FB exactly the logged box size, zpos >= 1.
|
||||
Commits pipeline, so a single snapshot may catch an intermediate
|
||||
state -- callers vote over several."""
|
||||
bw, bh, _, _ = box
|
||||
cands = []
|
||||
for (pid, crtc, fb, fw, fh, zpos) in _all_planes(card).values():
|
||||
if crtc and fb and fw == bw and fh == bh and zpos is not None \
|
||||
and zpos >= 1:
|
||||
cands.append(pid)
|
||||
for (pid, typ, crtc, fb, fw, fh, zpos) in _drm_planes(card):
|
||||
if typ == 0 and crtc and fb and fw == bw and fh == bh \
|
||||
and zpos is not None and zpos >= 1:
|
||||
cands.append(pid)
|
||||
cands = sorted(set(cands))
|
||||
return cands[0] if len(cands) == 1 else None
|
||||
|
||||
|
||||
def _all_planes(card="/dev/dri/card1"):
|
||||
"""Overlay planes plus the primary (83) and cursor (657) planes:
|
||||
(id, crtc_id, fb_id, fb_w, fb_h, zpos). libliftoff may place any
|
||||
layer on any compatible plane, so the menu FB can legally land
|
||||
outside the overlay list -- what matters is that exactly one
|
||||
topmost (zpos >= 1) plane carries it."""
|
||||
import ctypes
|
||||
lib = ctypes.CDLL("libdrm.so.2")
|
||||
|
||||
class Plane(ctypes.Structure):
|
||||
_fields_ = [("count_formats", ctypes.c_uint32),
|
||||
("formats", ctypes.POINTER(ctypes.c_uint32)),
|
||||
("plane_id", ctypes.c_uint32),
|
||||
("crtc_id", ctypes.c_uint32),
|
||||
("fb_id", ctypes.c_uint32),
|
||||
("crtc_x", ctypes.c_int32), ("crtc_y", ctypes.c_int32),
|
||||
("x", ctypes.c_uint32), ("y", ctypes.c_uint32),
|
||||
("possible_crtcs", ctypes.c_uint32),
|
||||
("gamma_size", ctypes.c_uint32)]
|
||||
|
||||
class PropPtrs(ctypes.Structure):
|
||||
_fields_ = [("count_props", ctypes.c_uint32),
|
||||
("props", ctypes.POINTER(ctypes.c_uint32)),
|
||||
("prop_values", ctypes.POINTER(ctypes.c_uint64))]
|
||||
|
||||
class Prop(ctypes.Structure):
|
||||
_fields_ = [("prop_id", ctypes.c_uint32), ("flags", ctypes.c_uint32),
|
||||
("name", ctypes.c_char * 32),
|
||||
("count_values", ctypes.c_uint32),
|
||||
("values", ctypes.POINTER(ctypes.c_uint64))]
|
||||
|
||||
class FB(ctypes.Structure):
|
||||
_fields_ = [("fb_id", ctypes.c_uint32),
|
||||
("width", ctypes.c_uint32), ("height", ctypes.c_uint32),
|
||||
("pitch", ctypes.c_uint32), ("bpp", ctypes.c_uint32),
|
||||
("depth", ctypes.c_uint32), ("handle", ctypes.c_uint32)]
|
||||
|
||||
lib.drmModeGetPlaneResources.restype = ctypes.c_void_p
|
||||
lib.drmModeGetPlane.restype = ctypes.POINTER(Plane)
|
||||
lib.drmModeGetPlane.argtypes = [ctypes.c_int, ctypes.c_uint32]
|
||||
lib.drmModeObjectGetProperties.restype = ctypes.POINTER(PropPtrs)
|
||||
lib.drmModeObjectGetProperties.argtypes = [ctypes.c_int, ctypes.c_uint32,
|
||||
ctypes.c_uint32]
|
||||
lib.drmModeGetProperty.restype = ctypes.POINTER(Prop)
|
||||
lib.drmModeGetProperty.argtypes = [ctypes.c_int, ctypes.c_uint32]
|
||||
lib.drmModeGetFB.restype = ctypes.POINTER(FB)
|
||||
lib.drmModeGetFB.argtypes = [ctypes.c_int, ctypes.c_uint32]
|
||||
lib.drmModeFreePlane.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreeObjectProperties.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreeProperty.argtypes = [ctypes.c_void_p]
|
||||
lib.drmModeFreeFB.argtypes = [ctypes.c_void_p]
|
||||
|
||||
def one(fd, pid):
|
||||
pl = lib.drmModeGetPlane(fd, pid)
|
||||
if not pl:
|
||||
return None
|
||||
c = pl.contents
|
||||
zpos = None
|
||||
pp = lib.drmModeObjectGetProperties(fd, pid, 0xeeeeeeee)
|
||||
if pp:
|
||||
for j in range(pp.contents.count_props):
|
||||
pr = lib.drmModeGetProperty(fd, pp.contents.props[j])
|
||||
if pr.contents.name.decode() == "zpos":
|
||||
zpos = pp.contents.prop_values[j]
|
||||
lib.drmModeFreeProperty(pr)
|
||||
lib.drmModeFreeObjectProperties(pp)
|
||||
fw = fh = None
|
||||
if c.fb_id:
|
||||
fb = lib.drmModeGetFB(fd, c.fb_id)
|
||||
if fb:
|
||||
fw, fh = fb.contents.width, fb.contents.height
|
||||
lib.drmModeFreeFB(fb)
|
||||
lib.drmModeFreePlane(pl)
|
||||
return (pid, c.crtc_id, c.fb_id, fw, fh, zpos)
|
||||
|
||||
out = {}
|
||||
fd = os.open(card, os.O_RDWR)
|
||||
try:
|
||||
for pid in (83, 107, 657):
|
||||
out[pid] = one(fd, pid)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return out
|
||||
|
||||
|
||||
def _assert_menu_plane_on_top(card, box, ctx, samples=5):
|
||||
"""The overlay menu must be scanned out on exactly one overlay plane:
|
||||
active CRTC, live FB sized exactly like the logged box, zpos >= 1
|
||||
(above the zpos-0 primary). Votes over several snapshots so one
|
||||
pipelined-commit race cannot fail the run; reports every sample."""
|
||||
if box is None:
|
||||
raise AssertionError("no logged overlay box (%s)" % ctx)
|
||||
seen = []
|
||||
for _ in range(samples):
|
||||
seen.append(_menu_plane_sample(card, box))
|
||||
time.sleep(0.15)
|
||||
good = sum(1 for m in seen if m is not None)
|
||||
print("menu plane votes (%s): %s" % (ctx, seen))
|
||||
assert good >= (samples // 2 + 1), \
|
||||
"menu plane missing/not on top (%s): %d/%d samples, last planes:\n%s" % (
|
||||
ctx, good, samples, _drm_planes(card))
|
||||
return seen[-1] if seen[-1] is not None else seen[0]
|
||||
|
||||
|
||||
def _assert_no_menu_plane(card, box, ctx):
|
||||
"""After release no overlay plane may still carry the menu FB size."""
|
||||
if box is None:
|
||||
return
|
||||
bw, bh, _, _ = box
|
||||
stale = [p for p in _drm_planes(card)
|
||||
if p[1] == 0 and p[2] and p[3] and p[4] == bw and p[5] == bh]
|
||||
assert not stale, "menu plane stuck on screen (%s): %r" % (ctx, stale)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def vt_foot_session():
|
||||
if not _vt_drm_available():
|
||||
pytest.skip("not on a DRM-capable VT (no card grants master)")
|
||||
# Serialize against other workers: two kicks would fight for master.
|
||||
with open(_VT_LOCK, "w") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
if not _vt_drm_available():
|
||||
pytest.skip("DRM master went away before spawn")
|
||||
for path in (BIN,):
|
||||
if not os.access(path, os.X_OK):
|
||||
pytest.skip("build first (DRM backend): %s" % path)
|
||||
log = _fresh_log()
|
||||
dump_dir = _mk_dump_dir()
|
||||
env = dict(os.environ)
|
||||
# Screen mode for the DRM output (KICK_TEST_SCREENMODE, default
|
||||
# 1920x1080): also try the connector preferred mode (4K here) for
|
||||
# HVS bandwidth pressure via KICK_TEST_SCREENMODE=3840x2160.
|
||||
screen_mode = env.get("KICK_TEST_SCREENMODE", "1920x1080")
|
||||
out_w, out_h = (int(x) for x in screen_mode.split("x"))
|
||||
cmd = [
|
||||
"env", "-i",
|
||||
"PATH=" + env["PATH"],
|
||||
"HOME=" + env.get("HOME", "/"),
|
||||
"XDG_RUNTIME_DIR=" + env.get("XDG_RUNTIME_DIR", "/tmp"),
|
||||
"XDG_DATA_HOME=" + env.get("XDG_DATA_HOME", ""),
|
||||
"XDG_DATA_DIRS=" + env.get("XDG_DATA_DIRS", ""),
|
||||
"KICK_LOG=2",
|
||||
"KICK_MODE=standalone",
|
||||
"WLR_BACKENDS=drm",
|
||||
"KICK_FONT=" + (env.get("KICK_FONT") or _pick_font() or ""),
|
||||
"KICK_DUMP_DIR=" + dump_dir,
|
||||
"KICK_TEST_SCREENMODE=" + screen_mode,
|
||||
"stdbuf", "-oL",
|
||||
BIN, "--screen-mode", screen_mode, "foot",
|
||||
]
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdin=subprocess.PIPE,
|
||||
stdout=open(log, "ab"), stderr=subprocess.STDOUT,
|
||||
preexec_fn=_preexec_death(os.getpid()))
|
||||
s = Session(proc, log, dump_dir)
|
||||
try:
|
||||
yield s
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def test_vt_overlay_survives_alacritty_format_switch(vt_foot_session):
|
||||
"""foot (shm) + Alt-held plane probe, then Alt+Shift+T alacritty
|
||||
(dmabuf) while held: the layer must stay accepted across the switch."""
|
||||
s = vt_foot_session
|
||||
_assert_alive(s, "kick exited early on the VT")
|
||||
assert s.wait_any_active() == 0
|
||||
s.wait_active_frame()
|
||||
_assert_alive(s, "kick died after first frame")
|
||||
|
||||
pos = len(s.read_log())
|
||||
s.mods(ALT)
|
||||
s.wait_for_new_after(pos, r"\[render\] overlay show")
|
||||
try:
|
||||
s.wait_for_new_after(pos, r"\[render\] overlay plane available",
|
||||
timeout=30.0)
|
||||
except Exception:
|
||||
pytest.fail("plane never became available (rejected at probe?):\n"
|
||||
+ _liftoff_excerpt(s.read_log()))
|
||||
s.wait_for_new_after(pos, r"\[diag\] frame (?:start|result) .*vis=1 plane=1")
|
||||
_assert_alive(s, "kick died while Alt held over foot")
|
||||
|
||||
# Alt+Shift+T (T is keycode 20), keep Alt held throughout.
|
||||
mark = len(s.read_log())
|
||||
s.mods(ALT | SHIFT)
|
||||
s.key(20, KEY_PRESSED)
|
||||
s.key(20, KEY_RELEASED)
|
||||
s.mods(ALT)
|
||||
s.wait_for_new_after(mark, r"\[launch\] app=\d+ pid=\d+ cmd=alacritty",
|
||||
timeout=30.0)
|
||||
# The fresh terminal becomes active once it maps.
|
||||
deadline = time.monotonic() + 60.0
|
||||
while time.monotonic() < deadline:
|
||||
_assert_alive(s, "kick died waiting for alacritty")
|
||||
try:
|
||||
if s.active() == 1:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
assert s.active() == 1, \
|
||||
"alacritty never became active:\n%s" % s.read_log()[-1500:]
|
||||
|
||||
# Let several plane frames land over alacritty, then require every
|
||||
# accepted sample since the switch chord to be 1. The offset predates
|
||||
# the chord: the switch's own frames qualify, so there is no race with
|
||||
# observing the effect first.
|
||||
s.wait_for_new_after(
|
||||
mark, r"\[render\] overlay plane accepted=1", timeout=30.0)
|
||||
time.sleep(2.0)
|
||||
tail = s.read_log()[mark:]
|
||||
verdicts = re_accept.findall(tail)
|
||||
assert verdicts, "no accepted samples over alacritty:\n%s" % tail[-1500:]
|
||||
assert set(verdicts) == {"1"}, \
|
||||
"overlay layer rejected over alacritty %r:\n%s" % (
|
||||
verdicts, _liftoff_excerpt(s.read_log()))
|
||||
|
||||
# Cycle back and confirm the plane is still accepted. Offset again
|
||||
# predates the key press for the same reason.
|
||||
before = s.active()
|
||||
cyc = len(s.read_log())
|
||||
s.key(DOWN, KEY_PRESSED)
|
||||
s.key(DOWN, KEY_RELEASED)
|
||||
deadline = time.monotonic() + 30.0
|
||||
while time.monotonic() < deadline:
|
||||
_assert_alive(s, "kick died cycling")
|
||||
try:
|
||||
if s.active() != before:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
assert s.active() != before, \
|
||||
"never cycled away from %d:\n%s" % (before, s.read_log()[-1500:])
|
||||
s.wait_for_new_after(
|
||||
cyc, r"\[render\] overlay plane accepted=1", timeout=30.0)
|
||||
|
||||
# Kernel-side stacking check while held: the menu must be on a small
|
||||
# overlay plane above the primary over BOTH tabs, with cursor motion
|
||||
# and held-key repeats churning the libliftoff allocation in between.
|
||||
# accepted=1 only means "placed somewhere" -- matching the logged box
|
||||
# size proves it is ours, zpos >= 1 proves "on top".
|
||||
card = _drm_card_with_connected()
|
||||
assert card, "DRM card went away mid-test"
|
||||
box = _logged_box(s)
|
||||
_assert_menu_plane_on_top(card, box, "over foot")
|
||||
_assert_menu_placed(card, box, "over foot")
|
||||
for x, y in ((960, 540), (200, 200), (1700, 900), (960, 540)):
|
||||
s.warp(x, y)
|
||||
time.sleep(0.3)
|
||||
time.sleep(3.0)
|
||||
_assert_menu_plane_on_top(card, _logged_box(s),
|
||||
"over foot after warps")
|
||||
# Held Down at the production repeat rate walks foot<->alacritty many
|
||||
# times; the stack must survive all of it.
|
||||
s.key(DOWN, KEY_PRESSED)
|
||||
time.sleep(2.0)
|
||||
s.key(DOWN, KEY_RELEASED)
|
||||
_assert_menu_plane_on_top(card, _logged_box(s),
|
||||
"after held-Down repeats")
|
||||
s.key(UP, KEY_PRESSED)
|
||||
time.sleep(2.0)
|
||||
s.key(UP, KEY_RELEASED)
|
||||
held_at = len(s.read_log())
|
||||
time.sleep(2.0)
|
||||
_assert_menu_plane_on_top(card, _logged_box(s),
|
||||
"after held-Up repeats")
|
||||
_assert_menu_placed(card, _logged_box(s), "after held-Up repeats")
|
||||
tail = s.read_log()[held_at:]
|
||||
assert "overlay hide" not in tail, \
|
||||
"overlay hid mid-hold:\n%s" % tail[-1500:]
|
||||
bad = [v for v in re_accept.findall(tail) if v != "1"]
|
||||
assert not bad, \
|
||||
"overlay layer rejected mid-hold %r:\n%s" % (
|
||||
bad, _liftoff_excerpt(s.read_log()))
|
||||
|
||||
pos = len(s.read_log())
|
||||
s.mods(0)
|
||||
s.wait_for_new_after(pos, r"\[render\] overlay hide\b")
|
||||
s.wait_for_new_after(pos, r"\[diag\] frame (?:start|result) .*vis=0 plane=0")
|
||||
box = _logged_box(s)
|
||||
deadline = time.monotonic() + 10.0
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
_assert_no_menu_plane(card, box, "after release")
|
||||
break
|
||||
except AssertionError:
|
||||
time.sleep(0.5)
|
||||
_assert_no_menu_plane(card, box, "after release")
|
||||
_assert_alive(s)
|
||||
Loading…
Add table
Add a link
Reference in a new issue