Park the hardware cursor plane while the overlay is held and paint the themed cursor image into the plane buffer after the menu, so the stack is cursor, overlay, content by construction on every backend: no cursor layer exists for the menu layer to trade planes with, and the menu buffer carries its own cursor wherever it lands. On the composite path re-add software cursors after the menu pass (a no-op while a hardware cursor plane is active). Extend the VT test with kernel-side placement (debugfs crtc-pos and normalized-zpos must match the logged box on top) and a single-snapshot cursor-above-menu order check.
628 lines
25 KiB
Python
628 lines
25 KiB
Python
#!/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 _assert_cursor_above_menu(card, box, ctx, samples=5):
|
|
"""Stacking order top-to-bottom must be cursor, menu, content, read
|
|
from a SINGLE debugfs snapshot per sample (plane reads across two
|
|
ioctls can straddle a commit). The menu matches by FB size and
|
|
programmed box; the primary is the fullscreen plane; any other small
|
|
active plane is the cursor and must sit above the menu. Votes."""
|
|
if box is None:
|
|
raise AssertionError("no logged overlay box (%s)" % ctx)
|
|
bw, bh, bx, by_ = box
|
|
goods = []
|
|
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]
|
|
menu = [pid for pid, p in planes.items()
|
|
if p.get("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_]
|
|
ok = bool(prims) and len(menu) == 1 and \
|
|
planes[menu[0]].get("zpos", 0) > 0
|
|
if ok:
|
|
mz = planes[menu[0]]["zpos"]
|
|
small = [(pid, p) for pid, p in planes.items()
|
|
if pid != menu[0] and p.get("crtc") and p.get("fb")
|
|
and p.get("fb_w") is not None and p.get("fb_h") is not None
|
|
and p.get("fb_w") <= 256 and p.get("fb_h") <= 256]
|
|
ok = all(p.get("zpos", 0) > mz for _, p in small)
|
|
goods.append(ok)
|
|
print("stack vote (%s): menu=%s cursor-above=%s" % (
|
|
ctx, menu[0] if ok and menu else None, ok))
|
|
time.sleep(0.15)
|
|
assert sum(goods) >= (samples // 2 + 1), \
|
|
"bad stacking (%s): %d/%d" % (ctx, sum(goods), samples)
|
|
|
|
|
|
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")
|
|
_assert_cursor_above_menu(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)
|