feat: present standalone overlay on a hardware plane, repaint via per-frame hash poll
On DRM standalone outputs the Alt tab list / confirm-quit modal is scanned out on a hardware plane via wlr_output_layer (libliftoff), so the primary buffer stays untouched and direct scanout of the focused client keeps running underneath. Plane capability is probed once per output from the first overlay commit and forced via WLR_DRM_FORCE_LIBLIFTOFF; KICK_OVERLAY_PLANE=0 keeps the composite fallback. Unlock the plane buffer only after commit, destroy the per-frame layer after state finish, and recompute the attach-render lock from capability on output recreate. Overlay freshness no longer depends on scattered explicit refresh calls: while held, each frame decision point hashes what the overlay would draw (rows, titles, highlight, icons) and repaints on drift. The confirm-quit modal stays on its explicit damage path. Adds test_alt_shift_t_refreshes_held_overlay.
This commit is contained in:
parent
df65452f62
commit
137e4d0568
4 changed files with 652 additions and 214 deletions
20
README.md
20
README.md
|
|
@ -343,6 +343,22 @@ ignored. In this mode kick takes over the screen, and the tab overlay is
|
||||||
composited onto the output in a second render pass instead of being drawn
|
composited onto the output in a second render pass instead of being drawn
|
||||||
into a host window.
|
into a host window.
|
||||||
|
|
||||||
|
On a real DRM output the overlay (the `Alt` tab list and the confirm-quit
|
||||||
|
modal) is presented on a **hardware plane** instead: its own small buffer is
|
||||||
|
scanned out by libliftoff (wlroots' `wlr_output_layer` path), so the main
|
||||||
|
buffer never carries the overlay and direct scanout of a focused fullscreen
|
||||||
|
client keeps running underneath it. The plane is probed once per output: the
|
||||||
|
first overlay hold commits to the plane, and whether libliftoff accepted the
|
||||||
|
layer (fixed hardware gives a fixed answer) sets the verdict for the whole
|
||||||
|
session -- every frame after that just asks the plane with a buffer or
|
||||||
|
without, and the commit itself arms or disarms it. If the probe finds no
|
||||||
|
plane (too few planes, format or placement constraints, multi-GPU rendering),
|
||||||
|
kick stays on the composite second pass for the session; the headless backend
|
||||||
|
is always composite (it would claim every layer "accepted" while drawing
|
||||||
|
nothing). `KICK_OVERLAY_PLANE=0` forces the composite path everywhere for A/B
|
||||||
|
comparison; the verdict is logged by `[render] overlay plane available` /
|
||||||
|
`[render] overlay plane not available, compositing`.
|
||||||
|
|
||||||
The auto-detection fires only when none of `KICK_MODE`, `WLR_BACKENDS`,
|
The auto-detection fires only when none of `KICK_MODE`, `WLR_BACKENDS`,
|
||||||
`WAYLAND_DISPLAY` or `DISPLAY` is set **and** either kick's stdin is a real
|
`WAYLAND_DISPLAY` or `DISPLAY` is set **and** either kick's stdin is a real
|
||||||
VT (`/dev/tty<digits>`) or the login advertises a Wayland session
|
VT (`/dev/tty<digits>`) or the login advertises a Wayland session
|
||||||
|
|
@ -579,7 +595,9 @@ files to `/tmp` regardless of the log level:
|
||||||
- `SIGUSR1` writes `/tmp/host_N.ppm` — the host compositor's back
|
- `SIGUSR1` writes `/tmp/host_N.ppm` — the host compositor's back
|
||||||
buffer, so you can see what the host actually displayed. In standalone
|
buffer, so you can see what the host actually displayed. In standalone
|
||||||
mode this is the composited frame being presented to the output (scene
|
mode this is the composited frame being presented to the output (scene
|
||||||
plus the `Alt` overlay).
|
plus the `Alt` overlay when the overlay rides the composite path; when it
|
||||||
|
is scanned out on a hardware plane it lives outside the frame buffer, so
|
||||||
|
dumps won't show it).
|
||||||
- `N` is an incrementing per-run counter; a single `SIGUSR1` produces a
|
- `N` is an incrementing per-run counter; a single `SIGUSR1` produces a
|
||||||
matching scene/host pair.
|
matching scene/host pair.
|
||||||
- With `KICK_AUTODUMP` (any value, e.g. `KICK_AUTODUMP=1`), a scene and
|
- With `KICK_AUTODUMP` (any value, e.g. `KICK_AUTODUMP=1`), a scene and
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@ Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=kick
|
Name=kick
|
||||||
Comment=Nested Wayland compositor presenting client apps as tabs in a single window
|
Comment=Nested Wayland compositor presenting client apps as tabs in a single window
|
||||||
Exec=env KICK_MODE=standalone kick
|
Exec=kick-pi --screen-mode 1920x1080@60.000 --panel wf-panel-pi --focus-priority firefox lxterminal
|
||||||
DesktopNames=kick;
|
DesktopNames=kick;
|
||||||
|
|
|
||||||
|
|
@ -3343,6 +3343,69 @@ def test_alt_shift_t_becomes_active(session):
|
||||||
os.environ["TERMINAL"] = old_term
|
os.environ["TERMINAL"] = old_term
|
||||||
|
|
||||||
|
|
||||||
|
def test_alt_shift_t_refreshes_held_overlay():
|
||||||
|
"""Alt+Shift+T while the overlay is held must repaint it at once.
|
||||||
|
|
||||||
|
Regression: add_tab scheduled no frame, so a held overlay kept showing
|
||||||
|
the old tab list until unrelated damage arrived -- on the hardware-plane
|
||||||
|
path the stale box was even scanned out as-is. The fresh (still-black)
|
||||||
|
tab must appear before its client maps, so TERMINAL never connects.
|
||||||
|
Headless standalone exercises the shared overlay/frame path.
|
||||||
|
"""
|
||||||
|
for path in (BIN, WL):
|
||||||
|
if not os.access(path, os.X_OK):
|
||||||
|
pytest.skip(f"build first: {path}")
|
||||||
|
log = _fresh_log()
|
||||||
|
dump_dir = _mk_dump_dir()
|
||||||
|
env = dict(os.environ)
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
["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=headless",
|
||||||
|
"KICK_FONT=" + (env.get("KICK_FONT") or _pick_font() or ""),
|
||||||
|
"XCURSOR_THEME=Adwaita",
|
||||||
|
"KICK_DUMP_DIR=" + dump_dir,
|
||||||
|
"KICK_REPEAT_DELAY_MS=" + KICK_REPEAT_TEST_DELAY,
|
||||||
|
"TERMINAL=sleep 30",
|
||||||
|
"stdbuf", "-oL",
|
||||||
|
BIN, f"{WL} '{RED}'", f"{WL} '{BLUE}'"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=open(log, "ab"), stderr=subprocess.STDOUT,
|
||||||
|
preexec_fn=_preexec_death(os.getpid()),
|
||||||
|
)
|
||||||
|
s = Session(proc, log, dump_dir)
|
||||||
|
try:
|
||||||
|
s.wait_any_active()
|
||||||
|
s.wait_active_frame()
|
||||||
|
pos = len(s.read_log())
|
||||||
|
s.mods(ALT)
|
||||||
|
s.wait_for_new_after(pos, r"\[render\] overlay show")
|
||||||
|
s.wait_for_new_after(pos, r"\[diag\] frame start .*vis=1")
|
||||||
|
mark = len(s.read_log())
|
||||||
|
s.mods(ALT | SHIFT)
|
||||||
|
s.key(20, KEY_PRESSED)
|
||||||
|
s.key(20, KEY_RELEASED)
|
||||||
|
s.mods(ALT)
|
||||||
|
# The new tab launches (and sleep never connects, so no map can
|
||||||
|
# rescue the overlay): a held-overlay frame showing its row must
|
||||||
|
# follow the launch itself.
|
||||||
|
s.wait_for_new_after(mark, r"\[launch\] app=\d+ pid=\d+ cmd=sleep 30")
|
||||||
|
off = s.read_log().index("cmd=sleep 30")
|
||||||
|
s.wait_for_new_after(off, r"\[title\] app=\d+ 'sleep 30'")
|
||||||
|
s.wait_for_new_after(off, r"\[diag\] frame start .*vis=1")
|
||||||
|
_assert_alive(s)
|
||||||
|
finally:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
s.mods(0)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
def test_sub_window_hidden_when_parent_not_active():
|
def test_sub_window_hidden_when_parent_not_active():
|
||||||
"""--tab-groups off: a second toplevel from an already-running client gets
|
"""--tab-groups off: a second toplevel from an already-running client gets
|
||||||
its own slot glued right after the parent tab, and because the parent is
|
its own slot glued right after the parent tab, and because the parent is
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue