diff --git a/kick.c b/kick.c index ecd244e..9e7d3a7 100644 --- a/kick.c +++ b/kick.c @@ -8706,8 +8706,14 @@ static void standalone_overlay_damage(void) { to re-damage that whole region; restore the z-order immediately so the always-enabled transparent rect never intercepts pointer hit-testing. */ wlr_scene_node_raise_to_top(&r->node); - wlr_scene_rect_set_size(r, r->width, r->height - 1); - wlr_scene_rect_set_size(r, r->width, r->height); + /* Snapshot the size: set_size updates the struct in place, so re-reading + r->height for the restore would keep the decremented value and shrink + the rect one pixel per call -- after output_height calls the height + reaches zero and the next bump aborts in wlr_scene_rect_set_size. + Every pointer motion calls here, so any real session got there. */ + int w = r->width, h = r->height; + wlr_scene_rect_set_size(r, w, h > 0 ? h - 1 : h); + wlr_scene_rect_set_size(r, w, h); wlr_scene_node_lower_to_bottom(&r->node); } @@ -10945,8 +10951,15 @@ static bool host_init(void) { return true; } - host.width = 1280; - host.height = 800; + /* An eager host (kick-in-kick) may have configured the window inside + the startup roundtrip already; keep that real size. Otherwise install + the default until the first (async) configure overwrites it. + Resetting unconditionally desyncs the swapchain (sized from the + configure) from the host size and trips wlroots' build_state assert. */ + if (!host.configured) { + host.width = 1280; + host.height = 800; + } host.egl_window = wl_egl_window_create(host.surface, host.width, host.height); host.egl_surface = eglCreateWindowSurface(host.egl_display, host.egl_config, (EGLNativeWindowType)host.egl_window, NULL); @@ -12709,6 +12722,14 @@ int main(int argc, char *argv[]) { return 1; } + /* The tab/MRU lists must exist before host_init(): a host that sends + its first configure synchronously inside the startup roundtrip drives + the resize path (which walks server.tabs) before any tab exists. */ + wl_list_init(&server.tabs); + wl_list_init(&server.mru); + server.mru_alt_anchor = NULL; + server.mru_alt_navigated = false; + /* Standalone mode presents onto the native output directly; nested mode connects to the host compositor and presents inside it. */ if (!server.standalone && !host_init()) { @@ -12723,12 +12744,8 @@ int main(int argc, char *argv[]) { order, each carrying a still-black pending window the command's client claims when it connects (the process-group match in handle_new_toplevel puts each client on its own slot no matter which - connects first). */ - wl_list_init(&server.tabs); - wl_list_init(&server.mru); - server.mru_alt_anchor = NULL; - server.mru_alt_navigated = false; - + connects first). The lists themselves were initialized before + host_init above. */ int focus_idx = start_cmd_index >= 0 ? start_cmd_index : total_apps - 1; struct kick_tab *focus_tab = NULL; for (int i = 0; i < total_apps; i++) { diff --git a/tests/test_smoke.py b/tests/test_smoke.py index a4cfcf7..66ceb97 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -836,6 +836,32 @@ def test_standalone_headless_overlay_clears_on_alt_release(standalone_session): _assert_alive(s) +def test_standalone_overlay_damage_survives_motion_burst(tmp_path): + """The overlay damage sentinel's bump must not shrink the rect: it used + to re-read its own height for the restore call, shaving one pixel per + invocation until the height hit zero and the next bump aborted in + wlr_scene_rect_set_size (width/height >= 0). Every pointer motion calls + it, so ~one output height's worth of motions (800 here) killed any + session that moved the mouse enough. Needs a cursor theme: without one + the motion listener never attaches and nothing bumps.""" + if not os.path.exists("/usr/share/icons/Adwaita/cursors"): + pytest.skip("needs a cursor theme so the motion listener attaches") + with _run_standalone({"XCURSOR_THEME": "Adwaita"}, + f"{WL} '{RED}'", wait="any_frame") as s: + # In chunks with a frame wait between them: libwayland dispatches + # every queued event before rendering, so each wait proves its + # chunk's motions were processed (a single trailing wait would let + # teardown reap the session with most of the burst still queued). + for chunk in range(12): + for i in range(100): + j = chunk * 100 + i + s.warp(100 if j % 2 == 0 else 1100, + 100 if j % 2 == 0 else 700) + s.wait_active_frame() + _assert_alive(s, "compositor died under motion burst " + "(sentinel rect shrank to zero)") + + def test_standalone_headless_overlay_title_chip_grey(standalone_session): """Every overlay title sits on its own dark-grey chip (0.22/0.22/0.25, i.e. RGB ~56/56/64 -- distinct from the box interior ~18/18/23, the box